From d469a89a98bdda637298aabad7a0d97b977066d5 Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Sat, 29 Jun 2024 15:21:42 -0500 Subject: [PATCH 001/333] Adding documentation describing how the project is managed, and adding a few scripts to assist with this --- .github/pull_request_template.md | 21 + CONTRIBUTING.md | 128 ++++ .../Sources/meshtastic/apponly.pb.swift | 95 +-- .../Sources/meshtastic/config.pb.swift | 337 +++++++--- .../Sources/meshtastic/mesh.pb.swift | 600 ++++++++---------- README.md | 15 +- RELEASING.md | 45 ++ protobufs | 2 +- scripts/create-release-branch.sh | 39 ++ 9 files changed, 780 insertions(+), 502 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md create mode 100644 RELEASING.md create mode 100644 scripts/create-release-branch.sh diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..788e7a2e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## What changed? + + +## Why did it change? + + +## How is this tested? + + +## Screenshots/Videos (when applicable) + + + +## Checklist + +- [ ] My code adheres to the project's coding and style guidelines. +- [ ] I have conducted a self-review of my code. +- [ ] I have commented my code, particularly in complex areas. +- [ ] I have made corresponding changes to the documentation. +- [ ] I have tested the change to ensure that it works as intended. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..94ade3d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,128 @@ +# Contributing to Meshtastic + +Thank you for considering contributing to Meshtastic! We appreciate your time and effort in helping to improve the project. This document outlines the guidelines for contributing to the project. + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Development Workflow](#development-workflow) + - [Targeting `main`](#targeting-main) + - [Small, Incremental Changes](#small-incremental-changes) + - [Rebase Commits](#rebase-commits) +3. [Creating a Branch](#creating-a-branch) +4. [Making Changes](#making-changes) +5. [Commit Messages](#commit-messages) +6. [Merging Changes](#merging-changes) +7. [Testing](#testing) +8. [Code Review](#code-review) +9. [Documentation](#documentation) +10. [Style Guides](#style-guides) + - [Git Commit Messages](#git-commit-messages) + - [Code Style](#code-style) +11. [Community](#community) + +## Getting Started + +1. Fork the repository on GitLab. +2. Clone your fork to your local machine: + ```sh + git clone https://gitlab.com//Meshtastic-Apple.git + ``` +3. Navigate to the project directory: + ```sh + cd Meshtastic-Apple + ``` +4. Open the Meshtastic.xcworkspace + ```sh + open Meshtastic.xcworkspace + ``` + +## Development Workflow + +### Targeting `main` + +In accordance with trunk-based development, all changes should target the `main` branch. + +### Small, Incremental Changes + +To facilitate easy code reviews and minimize merge conflicts, we encourage making small, incremental changes. Each change should be a self-contained, logically coherent unit of work that addresses a specific task or fixes a particular issue. + +### Rebase Commits + +To keep the project history clean, please use rebasing over merging when incorporating changes from the `main` branch into your feature branches. To rebase your branch on `main`, you can perform the following steps. + +```sh +git fetch +git rebase main +``` + +To enable pulls to rebase by default, you can use this git configuration option. + +```sh +git config pull.rebase true +``` + +## Creating a Branch + +1. Always create a new branch for your work. Use a descriptive name for your branch: + ```sh + git checkout -b your-branch-name + ``` + +## Making Changes + +1. Make your changes in the new branch. +2. Ensure your changes adhere to the project’s coding standards and conventions. +3. Keep your changes focused and avoid combining multiple unrelated tasks in a single branch. + +## Commit Messages + +1. Write clear and concise commit messages following the guidelines in [Git Commit Messages](#git-commit-messages). + +## Merging Changes + +1. Push your changes to your fork: + ```sh + git push origin your-branch-name + ``` +2. Create a pull request (PR) targeting the `main` branch. +3. Ensure your PR adheres to the project's guidelines and includes a clear description of the changes. +4. Request a code review from the project maintainers. + +## Testing + +1. Ensure all existing tests pass before submitting your PR. +2. Write new tests for any new features or bug fixes. +3. Run the tests locally + +## Code Review + +1. Address any feedback or changes requested by the reviewers. +2. Once approved, the PR will be merged into the `main` branch by a project maintainer. + +## Documentation + +1. Update the documentation to reflect any changes you have made. +2. Ensure the documentation is clear and concise. + +## Style Guides + +### Git Commit Messages + +- Use the imperative mood in the subject line (e.g., "Fix bug" instead of "Fixed bug"). +- Use the body to explain what and why, not how. + +### Code Style + +- This project requires swiftLint - see https://github.com/realm/SwiftLint +- Use SwiftUI +- Use SFSymbols for icons +- Use Core Data for persistence +- Ensure your code is clean and well-documented. + +## Community + +- Join our community on [Discord](https://discord.com/invite/ktMAKGBnBs). +- Participate in discussions and share your ideas. + +Thank you for contributing to Meshtastic! \ No newline at end of file diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index d1533d59..0457077c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -33,27 +33,24 @@ public struct ChannelSet { /// /// Channel list with settings - public var settings: [ChannelSettings] { - get {return _storage._settings} - set {_uniqueStorage()._settings = newValue} - } + public var settings: [ChannelSettings] = [] /// /// LoRa config public var loraConfig: Config.LoRaConfig { - get {return _storage._loraConfig ?? Config.LoRaConfig()} - set {_uniqueStorage()._loraConfig = newValue} + get {return _loraConfig ?? Config.LoRaConfig()} + set {_loraConfig = newValue} } /// Returns true if `loraConfig` has been explicitly set. - public var hasLoraConfig: Bool {return _storage._loraConfig != nil} + public var hasLoraConfig: Bool {return self._loraConfig != nil} /// Clears the value of `loraConfig`. Subsequent reads from it will return its default value. - public mutating func clearLoraConfig() {_uniqueStorage()._loraConfig = nil} + public mutating func clearLoraConfig() {self._loraConfig = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _loraConfig: Config.LoRaConfig? = nil } #if swift(>=5.5) && canImport(_Concurrency) @@ -71,78 +68,36 @@ extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 2: .standard(proto: "lora_config"), ] - fileprivate class _StorageClass { - var _settings: [ChannelSettings] = [] - var _loraConfig: Config.LoRaConfig? = nil - - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif - - private init() {} - - init(copying source: _StorageClass) { - _settings = source._settings - _loraConfig = source._loraConfig - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - public mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - 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.decodeRepeatedMessageField(value: &_storage._settings) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._loraConfig) }() - default: break - } + 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.decodeRepeatedMessageField(value: &self.settings) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._loraConfig) }() + default: break } } } public func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // 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 !_storage._settings.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._settings, fieldNumber: 1) - } - try { if let v = _storage._loraConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() + // 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.settings.isEmpty { + try visitor.visitRepeatedMessageField(value: self.settings, fieldNumber: 1) } + try { if let v = self._loraConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: ChannelSet, rhs: ChannelSet) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._settings != rhs_storage._settings {return false} - if _storage._loraConfig != rhs_storage._loraConfig {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.settings != rhs.settings {return false} + if lhs._loraConfig != rhs._loraConfig {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index f6c42f70..f396367c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1087,7 +1087,10 @@ public struct Config { /// /// When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate` /// will be taked from their respective manually defined fields - public var usePreset: Bool = false + public var usePreset: Bool { + get {return _storage._usePreset} + set {_uniqueStorage()._usePreset = newValue} + } /// /// Either modem_config or bandwidth/spreading/coding will be specified - NOT BOTH. @@ -1095,51 +1098,78 @@ public struct Config { /// Because protobufs take ZERO space when the value is zero this works out nicely. /// This value is replaced by bandwidth/spread_factor/coding_rate. /// If you'd like to experiment with other options add them to MeshRadio.cpp in the device code. - public var modemPreset: Config.LoRaConfig.ModemPreset = .longFast + public var modemPreset: Config.LoRaConfig.ModemPreset { + get {return _storage._modemPreset} + set {_uniqueStorage()._modemPreset = newValue} + } /// /// Bandwidth in MHz /// Certain bandwidth numbers are 'special' and will be converted to the /// appropriate floating point value: 31 -> 31.25MHz - public var bandwidth: UInt32 = 0 + public var bandwidth: UInt32 { + get {return _storage._bandwidth} + set {_uniqueStorage()._bandwidth = newValue} + } /// /// A number from 7 to 12. /// Indicates number of chirps per symbol as 1< 7 results in the default - public var hopLimit: UInt32 = 0 + public var hopLimit: UInt32 { + get {return _storage._hopLimit} + set {_uniqueStorage()._hopLimit = newValue} + } /// /// Disable TX from the LoRa radio. Useful for hot-swapping antennas and other tests. /// Defaults to false - public var txEnabled: Bool = false + public var txEnabled: Bool { + get {return _storage._txEnabled} + set {_uniqueStorage()._txEnabled = newValue} + } /// /// If zero, then use default max legal continuous power (ie. something that won't /// burn out the radio hardware) /// In most cases you should use zero here. /// Units are in dBm. - public var txPower: Int32 = 0 + public var txPower: Int32 { + get {return _storage._txPower} + set {_uniqueStorage()._txPower = newValue} + } /// /// This controls the actual hardware frequency the radio transmits on. @@ -1149,17 +1179,26 @@ public struct Config { /// algorithm to derive the channel number") /// If using the hash algorithm the channel number will be: hash(channel_name) % /// NUM_CHANNELS (Where num channels depends on the regulatory region). - public var channelNum: UInt32 = 0 + public var channelNum: UInt32 { + get {return _storage._channelNum} + set {_uniqueStorage()._channelNum = newValue} + } /// /// If true, duty cycle limits will be exceeded and thus you're possibly not following /// the local regulations if you're not a HAM. /// Has no effect if the duty cycle of the used region is 100%. - public var overrideDutyCycle: Bool = false + public var overrideDutyCycle: Bool { + get {return _storage._overrideDutyCycle} + set {_uniqueStorage()._overrideDutyCycle = newValue} + } /// /// If true, sets RX boosted gain mode on SX126X based radios - public var sx126XRxBoostedGain: Bool = false + public var sx126XRxBoostedGain: Bool { + get {return _storage._sx126XRxBoostedGain} + set {_uniqueStorage()._sx126XRxBoostedGain = newValue} + } /// /// This parameter is for advanced users and licensed HAM radio operators. @@ -1167,17 +1206,33 @@ public struct Config { /// will still be applied. This will allow you to use out-of-band frequencies. /// Please respect your local laws and regulations. If you are a HAM, make sure you /// enable HAM mode and turn off encryption. - public var overrideFrequency: Float = 0 + public var overrideFrequency: Float { + get {return _storage._overrideFrequency} + set {_uniqueStorage()._overrideFrequency = newValue} + } + + /// + /// If true, disable the build-in PA FAN using pin define in RF95_FAN_EN. + public var paFanDisabled: Bool { + get {return _storage._paFanDisabled} + set {_uniqueStorage()._paFanDisabled = newValue} + } /// /// For testing it is useful sometimes to force a node to never listen to /// particular other nodes (simulating radio out of range). All nodenums listed /// in ignore_incoming will have packets they send dropped on receive (by router.cpp) - public var ignoreIncoming: [UInt32] = [] + public var ignoreIncoming: [UInt32] { + get {return _storage._ignoreIncoming} + set {_uniqueStorage()._ignoreIncoming = newValue} + } /// /// If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it. - public var ignoreMqtt: Bool = false + public var ignoreMqtt: Bool { + get {return _storage._ignoreMqtt} + set {_uniqueStorage()._ignoreMqtt = newValue} + } public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -1391,6 +1446,8 @@ public struct Config { } public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance } public struct BluetoothConfig { @@ -2443,106 +2500,184 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 12: .standard(proto: "override_duty_cycle"), 13: .standard(proto: "sx126x_rx_boosted_gain"), 14: .standard(proto: "override_frequency"), + 15: .standard(proto: "pa_fan_disabled"), 103: .standard(proto: "ignore_incoming"), 104: .standard(proto: "ignore_mqtt"), ] + fileprivate class _StorageClass { + var _usePreset: Bool = false + var _modemPreset: Config.LoRaConfig.ModemPreset = .longFast + var _bandwidth: UInt32 = 0 + var _spreadFactor: UInt32 = 0 + var _codingRate: UInt32 = 0 + var _frequencyOffset: Float = 0 + var _region: Config.LoRaConfig.RegionCode = .unset + var _hopLimit: UInt32 = 0 + var _txEnabled: Bool = false + var _txPower: Int32 = 0 + var _channelNum: UInt32 = 0 + var _overrideDutyCycle: Bool = false + var _sx126XRxBoostedGain: Bool = false + var _overrideFrequency: Float = 0 + var _paFanDisabled: Bool = false + var _ignoreIncoming: [UInt32] = [] + var _ignoreMqtt: Bool = false + + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif + + private init() {} + + init(copying source: _StorageClass) { + _usePreset = source._usePreset + _modemPreset = source._modemPreset + _bandwidth = source._bandwidth + _spreadFactor = source._spreadFactor + _codingRate = source._codingRate + _frequencyOffset = source._frequencyOffset + _region = source._region + _hopLimit = source._hopLimit + _txEnabled = source._txEnabled + _txPower = source._txPower + _channelNum = source._channelNum + _overrideDutyCycle = source._overrideDutyCycle + _sx126XRxBoostedGain = source._sx126XRxBoostedGain + _overrideFrequency = source._overrideFrequency + _paFanDisabled = source._paFanDisabled + _ignoreIncoming = source._ignoreIncoming + _ignoreMqtt = source._ignoreMqtt + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + 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.decodeSingularBoolField(value: &self.usePreset) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.modemPreset) }() - case 3: try { try decoder.decodeSingularUInt32Field(value: &self.bandwidth) }() - case 4: try { try decoder.decodeSingularUInt32Field(value: &self.spreadFactor) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self.codingRate) }() - case 6: try { try decoder.decodeSingularFloatField(value: &self.frequencyOffset) }() - case 7: try { try decoder.decodeSingularEnumField(value: &self.region) }() - case 8: try { try decoder.decodeSingularUInt32Field(value: &self.hopLimit) }() - case 9: try { try decoder.decodeSingularBoolField(value: &self.txEnabled) }() - case 10: try { try decoder.decodeSingularInt32Field(value: &self.txPower) }() - case 11: try { try decoder.decodeSingularUInt32Field(value: &self.channelNum) }() - case 12: try { try decoder.decodeSingularBoolField(value: &self.overrideDutyCycle) }() - case 13: try { try decoder.decodeSingularBoolField(value: &self.sx126XRxBoostedGain) }() - case 14: try { try decoder.decodeSingularFloatField(value: &self.overrideFrequency) }() - case 103: try { try decoder.decodeRepeatedUInt32Field(value: &self.ignoreIncoming) }() - case 104: try { try decoder.decodeSingularBoolField(value: &self.ignoreMqtt) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + 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.decodeSingularBoolField(value: &_storage._usePreset) }() + case 2: try { try decoder.decodeSingularEnumField(value: &_storage._modemPreset) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &_storage._bandwidth) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &_storage._spreadFactor) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &_storage._codingRate) }() + case 6: try { try decoder.decodeSingularFloatField(value: &_storage._frequencyOffset) }() + case 7: try { try decoder.decodeSingularEnumField(value: &_storage._region) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopLimit) }() + case 9: try { try decoder.decodeSingularBoolField(value: &_storage._txEnabled) }() + case 10: try { try decoder.decodeSingularInt32Field(value: &_storage._txPower) }() + case 11: try { try decoder.decodeSingularUInt32Field(value: &_storage._channelNum) }() + case 12: try { try decoder.decodeSingularBoolField(value: &_storage._overrideDutyCycle) }() + case 13: try { try decoder.decodeSingularBoolField(value: &_storage._sx126XRxBoostedGain) }() + case 14: try { try decoder.decodeSingularFloatField(value: &_storage._overrideFrequency) }() + case 15: try { try decoder.decodeSingularBoolField(value: &_storage._paFanDisabled) }() + case 103: try { try decoder.decodeRepeatedUInt32Field(value: &_storage._ignoreIncoming) }() + case 104: try { try decoder.decodeSingularBoolField(value: &_storage._ignoreMqtt) }() + default: break + } } } } public func traverse(visitor: inout V) throws { - if self.usePreset != false { - try visitor.visitSingularBoolField(value: self.usePreset, fieldNumber: 1) - } - if self.modemPreset != .longFast { - try visitor.visitSingularEnumField(value: self.modemPreset, fieldNumber: 2) - } - if self.bandwidth != 0 { - try visitor.visitSingularUInt32Field(value: self.bandwidth, fieldNumber: 3) - } - if self.spreadFactor != 0 { - try visitor.visitSingularUInt32Field(value: self.spreadFactor, fieldNumber: 4) - } - if self.codingRate != 0 { - try visitor.visitSingularUInt32Field(value: self.codingRate, fieldNumber: 5) - } - if self.frequencyOffset != 0 { - try visitor.visitSingularFloatField(value: self.frequencyOffset, fieldNumber: 6) - } - if self.region != .unset { - try visitor.visitSingularEnumField(value: self.region, fieldNumber: 7) - } - if self.hopLimit != 0 { - try visitor.visitSingularUInt32Field(value: self.hopLimit, fieldNumber: 8) - } - if self.txEnabled != false { - try visitor.visitSingularBoolField(value: self.txEnabled, fieldNumber: 9) - } - if self.txPower != 0 { - try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 10) - } - if self.channelNum != 0 { - try visitor.visitSingularUInt32Field(value: self.channelNum, fieldNumber: 11) - } - if self.overrideDutyCycle != false { - try visitor.visitSingularBoolField(value: self.overrideDutyCycle, fieldNumber: 12) - } - if self.sx126XRxBoostedGain != false { - try visitor.visitSingularBoolField(value: self.sx126XRxBoostedGain, fieldNumber: 13) - } - if self.overrideFrequency != 0 { - try visitor.visitSingularFloatField(value: self.overrideFrequency, fieldNumber: 14) - } - if !self.ignoreIncoming.isEmpty { - try visitor.visitPackedUInt32Field(value: self.ignoreIncoming, fieldNumber: 103) - } - if self.ignoreMqtt != false { - try visitor.visitSingularBoolField(value: self.ignoreMqtt, fieldNumber: 104) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + if _storage._usePreset != false { + try visitor.visitSingularBoolField(value: _storage._usePreset, fieldNumber: 1) + } + if _storage._modemPreset != .longFast { + try visitor.visitSingularEnumField(value: _storage._modemPreset, fieldNumber: 2) + } + if _storage._bandwidth != 0 { + try visitor.visitSingularUInt32Field(value: _storage._bandwidth, fieldNumber: 3) + } + if _storage._spreadFactor != 0 { + try visitor.visitSingularUInt32Field(value: _storage._spreadFactor, fieldNumber: 4) + } + if _storage._codingRate != 0 { + try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) + } + if _storage._frequencyOffset != 0 { + try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) + } + if _storage._region != .unset { + try visitor.visitSingularEnumField(value: _storage._region, fieldNumber: 7) + } + if _storage._hopLimit != 0 { + try visitor.visitSingularUInt32Field(value: _storage._hopLimit, fieldNumber: 8) + } + if _storage._txEnabled != false { + try visitor.visitSingularBoolField(value: _storage._txEnabled, fieldNumber: 9) + } + if _storage._txPower != 0 { + try visitor.visitSingularInt32Field(value: _storage._txPower, fieldNumber: 10) + } + if _storage._channelNum != 0 { + try visitor.visitSingularUInt32Field(value: _storage._channelNum, fieldNumber: 11) + } + if _storage._overrideDutyCycle != false { + try visitor.visitSingularBoolField(value: _storage._overrideDutyCycle, fieldNumber: 12) + } + if _storage._sx126XRxBoostedGain != false { + try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) + } + if _storage._overrideFrequency != 0 { + try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) + } + if _storage._paFanDisabled != false { + try visitor.visitSingularBoolField(value: _storage._paFanDisabled, fieldNumber: 15) + } + if !_storage._ignoreIncoming.isEmpty { + try visitor.visitPackedUInt32Field(value: _storage._ignoreIncoming, fieldNumber: 103) + } + if _storage._ignoreMqtt != false { + try visitor.visitSingularBoolField(value: _storage._ignoreMqtt, fieldNumber: 104) + } } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Config.LoRaConfig, rhs: Config.LoRaConfig) -> Bool { - if lhs.usePreset != rhs.usePreset {return false} - if lhs.modemPreset != rhs.modemPreset {return false} - if lhs.bandwidth != rhs.bandwidth {return false} - if lhs.spreadFactor != rhs.spreadFactor {return false} - if lhs.codingRate != rhs.codingRate {return false} - if lhs.frequencyOffset != rhs.frequencyOffset {return false} - if lhs.region != rhs.region {return false} - if lhs.hopLimit != rhs.hopLimit {return false} - if lhs.txEnabled != rhs.txEnabled {return false} - if lhs.txPower != rhs.txPower {return false} - if lhs.channelNum != rhs.channelNum {return false} - if lhs.overrideDutyCycle != rhs.overrideDutyCycle {return false} - if lhs.sx126XRxBoostedGain != rhs.sx126XRxBoostedGain {return false} - if lhs.overrideFrequency != rhs.overrideFrequency {return false} - if lhs.ignoreIncoming != rhs.ignoreIncoming {return false} - if lhs.ignoreMqtt != rhs.ignoreMqtt {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._usePreset != rhs_storage._usePreset {return false} + if _storage._modemPreset != rhs_storage._modemPreset {return false} + if _storage._bandwidth != rhs_storage._bandwidth {return false} + if _storage._spreadFactor != rhs_storage._spreadFactor {return false} + if _storage._codingRate != rhs_storage._codingRate {return false} + if _storage._frequencyOffset != rhs_storage._frequencyOffset {return false} + if _storage._region != rhs_storage._region {return false} + if _storage._hopLimit != rhs_storage._hopLimit {return false} + if _storage._txEnabled != rhs_storage._txEnabled {return false} + if _storage._txPower != rhs_storage._txPower {return false} + if _storage._channelNum != rhs_storage._channelNum {return false} + if _storage._overrideDutyCycle != rhs_storage._overrideDutyCycle {return false} + if _storage._sx126XRxBoostedGain != rhs_storage._sx126XRxBoostedGain {return false} + if _storage._overrideFrequency != rhs_storage._overrideFrequency {return false} + if _storage._paFanDisabled != rhs_storage._paFanDisabled {return false} + if _storage._ignoreIncoming != rhs_storage._ignoreIncoming {return false} + if _storage._ignoreMqtt != rhs_storage._ignoreMqtt {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 2b9f69c6..57e8bde4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -2165,26 +2165,20 @@ public struct FromRadio { /// /// The packet id, used to allow the phone to request missing read packets from the FIFO, /// see our bluetooth docs - public var id: UInt32 { - get {return _storage._id} - set {_uniqueStorage()._id = newValue} - } + public var id: UInt32 = 0 /// /// Log levels, chosen to match python logging conventions. - public var payloadVariant: OneOf_PayloadVariant? { - get {return _storage._payloadVariant} - set {_uniqueStorage()._payloadVariant = newValue} - } + public var payloadVariant: FromRadio.OneOf_PayloadVariant? = nil /// /// Log levels, chosen to match python logging conventions. public var packet: MeshPacket { get { - if case .packet(let v)? = _storage._payloadVariant {return v} + if case .packet(let v)? = payloadVariant {return v} return MeshPacket() } - set {_uniqueStorage()._payloadVariant = .packet(newValue)} + set {payloadVariant = .packet(newValue)} } /// @@ -2192,10 +2186,10 @@ public struct FromRadio { /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. public var myInfo: MyNodeInfo { get { - if case .myInfo(let v)? = _storage._payloadVariant {return v} + if case .myInfo(let v)? = payloadVariant {return v} return MyNodeInfo() } - set {_uniqueStorage()._payloadVariant = .myInfo(newValue)} + set {payloadVariant = .myInfo(newValue)} } /// @@ -2203,30 +2197,30 @@ public struct FromRadio { /// starts over with the first node in our DB public var nodeInfo: NodeInfo { get { - if case .nodeInfo(let v)? = _storage._payloadVariant {return v} + if case .nodeInfo(let v)? = payloadVariant {return v} return NodeInfo() } - set {_uniqueStorage()._payloadVariant = .nodeInfo(newValue)} + set {payloadVariant = .nodeInfo(newValue)} } /// /// Include a part of the config (was: RadioConfig radio) public var config: Config { get { - if case .config(let v)? = _storage._payloadVariant {return v} + if case .config(let v)? = payloadVariant {return v} return Config() } - set {_uniqueStorage()._payloadVariant = .config(newValue)} + set {payloadVariant = .config(newValue)} } /// /// Set to send debug console output over our protobuf stream public var logRecord: LogRecord { get { - if case .logRecord(let v)? = _storage._payloadVariant {return v} + if case .logRecord(let v)? = payloadVariant {return v} return LogRecord() } - set {_uniqueStorage()._payloadVariant = .logRecord(newValue)} + set {payloadVariant = .logRecord(newValue)} } /// @@ -2236,10 +2230,10 @@ public struct FromRadio { /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. public var configCompleteID: UInt32 { get { - if case .configCompleteID(let v)? = _storage._payloadVariant {return v} + if case .configCompleteID(let v)? = payloadVariant {return v} return 0 } - set {_uniqueStorage()._payloadVariant = .configCompleteID(newValue)} + set {payloadVariant = .configCompleteID(newValue)} } /// @@ -2249,80 +2243,80 @@ public struct FromRadio { /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. public var rebooted: Bool { get { - if case .rebooted(let v)? = _storage._payloadVariant {return v} + if case .rebooted(let v)? = payloadVariant {return v} return false } - set {_uniqueStorage()._payloadVariant = .rebooted(newValue)} + set {payloadVariant = .rebooted(newValue)} } /// /// Include module config public var moduleConfig: ModuleConfig { get { - if case .moduleConfig(let v)? = _storage._payloadVariant {return v} + if case .moduleConfig(let v)? = payloadVariant {return v} return ModuleConfig() } - set {_uniqueStorage()._payloadVariant = .moduleConfig(newValue)} + set {payloadVariant = .moduleConfig(newValue)} } /// /// One packet is sent for each channel public var channel: Channel { get { - if case .channel(let v)? = _storage._payloadVariant {return v} + if case .channel(let v)? = payloadVariant {return v} return Channel() } - set {_uniqueStorage()._payloadVariant = .channel(newValue)} + set {payloadVariant = .channel(newValue)} } /// /// Queue status info public var queueStatus: QueueStatus { get { - if case .queueStatus(let v)? = _storage._payloadVariant {return v} + if case .queueStatus(let v)? = payloadVariant {return v} return QueueStatus() } - set {_uniqueStorage()._payloadVariant = .queueStatus(newValue)} + set {payloadVariant = .queueStatus(newValue)} } /// /// File Transfer Chunk public var xmodemPacket: XModem { get { - if case .xmodemPacket(let v)? = _storage._payloadVariant {return v} + if case .xmodemPacket(let v)? = payloadVariant {return v} return XModem() } - set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)} + set {payloadVariant = .xmodemPacket(newValue)} } /// /// Device metadata message public var metadata: DeviceMetadata { get { - if case .metadata(let v)? = _storage._payloadVariant {return v} + if case .metadata(let v)? = payloadVariant {return v} return DeviceMetadata() } - set {_uniqueStorage()._payloadVariant = .metadata(newValue)} + set {payloadVariant = .metadata(newValue)} } /// /// MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) public var mqttClientProxyMessage: MqttClientProxyMessage { get { - if case .mqttClientProxyMessage(let v)? = _storage._payloadVariant {return v} + if case .mqttClientProxyMessage(let v)? = payloadVariant {return v} return MqttClientProxyMessage() } - set {_uniqueStorage()._payloadVariant = .mqttClientProxyMessage(newValue)} + set {payloadVariant = .mqttClientProxyMessage(newValue)} } /// /// File system manifest messages public var fileInfo: FileInfo { get { - if case .fileInfo(let v)? = _storage._payloadVariant {return v} + if case .fileInfo(let v)? = payloadVariant {return v} return FileInfo() } - set {_uniqueStorage()._payloadVariant = .fileInfo(newValue)} + set {payloadVariant = .fileInfo(newValue)} } public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2450,8 +2444,6 @@ public struct FromRadio { } public init() {} - - fileprivate var _storage = _StorageClass.defaultInstance } /// @@ -4303,305 +4295,263 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 15: .same(proto: "fileInfo"), ] - fileprivate class _StorageClass { - var _id: UInt32 = 0 - var _payloadVariant: FromRadio.OneOf_PayloadVariant? - - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif - - private init() {} - - init(copying source: _StorageClass) { - _id = source._id - _payloadVariant = source._payloadVariant - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - public mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - 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: &_storage._id) }() - case 2: try { - var v: MeshPacket? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .packet(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .packet(v) - } - }() - case 3: try { - var v: MyNodeInfo? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .myInfo(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .myInfo(v) - } - }() - case 4: try { - var v: NodeInfo? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .nodeInfo(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .nodeInfo(v) - } - }() - case 5: try { - var v: Config? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .config(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .config(v) - } - }() - case 6: try { - var v: LogRecord? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .logRecord(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .logRecord(v) - } - }() - case 7: try { - var v: UInt32? - try decoder.decodeSingularUInt32Field(value: &v) - if let v = v { - if _storage._payloadVariant != nil {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .configCompleteID(v) - } - }() - case 8: try { - var v: Bool? - try decoder.decodeSingularBoolField(value: &v) - if let v = v { - if _storage._payloadVariant != nil {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .rebooted(v) - } - }() - case 9: try { - var v: ModuleConfig? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .moduleConfig(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .moduleConfig(v) - } - }() - case 10: try { - var v: Channel? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .channel(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .channel(v) - } - }() - case 11: try { - var v: QueueStatus? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .queueStatus(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .queueStatus(v) - } - }() - case 12: try { - var v: XModem? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .xmodemPacket(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .xmodemPacket(v) - } - }() - case 13: try { - var v: DeviceMetadata? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .metadata(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .metadata(v) - } - }() - case 14: try { - var v: MqttClientProxyMessage? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .mqttClientProxyMessage(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .mqttClientProxyMessage(v) - } - }() - case 15: try { - var v: FileInfo? - var hadOneofValue = false - if let current = _storage._payloadVariant { - hadOneofValue = true - if case .fileInfo(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - _storage._payloadVariant = .fileInfo(v) - } - }() - default: break + 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.id) }() + case 2: try { + var v: MeshPacket? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .packet(let m) = current {v = m} } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .packet(v) + } + }() + case 3: try { + var v: MyNodeInfo? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .myInfo(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .myInfo(v) + } + }() + case 4: try { + var v: NodeInfo? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .nodeInfo(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .nodeInfo(v) + } + }() + case 5: try { + var v: Config? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .config(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .config(v) + } + }() + case 6: try { + var v: LogRecord? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .logRecord(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .logRecord(v) + } + }() + case 7: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .configCompleteID(v) + } + }() + case 8: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .rebooted(v) + } + }() + case 9: try { + var v: ModuleConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .moduleConfig(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .moduleConfig(v) + } + }() + case 10: try { + var v: Channel? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .channel(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .channel(v) + } + }() + case 11: try { + var v: QueueStatus? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .queueStatus(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .queueStatus(v) + } + }() + case 12: try { + var v: XModem? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .xmodemPacket(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .xmodemPacket(v) + } + }() + case 13: try { + var v: DeviceMetadata? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .metadata(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .metadata(v) + } + }() + case 14: try { + var v: MqttClientProxyMessage? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .mqttClientProxyMessage(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .mqttClientProxyMessage(v) + } + }() + case 15: try { + var v: FileInfo? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .fileInfo(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .fileInfo(v) + } + }() + default: break } } } public func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // 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 _storage._id != 0 { - try visitor.visitSingularUInt32Field(value: _storage._id, fieldNumber: 1) - } - switch _storage._payloadVariant { - case .packet?: try { - guard case .packet(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .myInfo?: try { - guard case .myInfo(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case .nodeInfo?: try { - guard case .nodeInfo(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .config?: try { - guard case .config(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .logRecord?: try { - guard case .logRecord(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .configCompleteID?: try { - guard case .configCompleteID(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) - }() - case .rebooted?: try { - guard case .rebooted(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularBoolField(value: v, fieldNumber: 8) - }() - case .moduleConfig?: try { - guard case .moduleConfig(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 9) - }() - case .channel?: try { - guard case .channel(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - }() - case .queueStatus?: try { - guard case .queueStatus(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - }() - case .xmodemPacket?: try { - guard case .xmodemPacket(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 12) - }() - case .metadata?: try { - guard case .metadata(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 13) - }() - case .mqttClientProxyMessage?: try { - guard case .mqttClientProxyMessage(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 14) - }() - case .fileInfo?: try { - guard case .fileInfo(let v)? = _storage._payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 15) - }() - case nil: break - } + // 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.id != 0 { + try visitor.visitSingularUInt32Field(value: self.id, fieldNumber: 1) + } + switch self.payloadVariant { + case .packet?: try { + guard case .packet(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .myInfo?: try { + guard case .myInfo(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .nodeInfo?: try { + guard case .nodeInfo(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .config?: try { + guard case .config(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .logRecord?: try { + guard case .logRecord(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .configCompleteID?: try { + guard case .configCompleteID(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + }() + case .rebooted?: try { + guard case .rebooted(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 8) + }() + case .moduleConfig?: try { + guard case .moduleConfig(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() + case .channel?: try { + guard case .channel(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case .queueStatus?: try { + guard case .queueStatus(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .xmodemPacket?: try { + guard case .xmodemPacket(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .metadata?: try { + guard case .metadata(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case .mqttClientProxyMessage?: try { + guard case .mqttClientProxyMessage(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case .fileInfo?: try { + guard case .fileInfo(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 15) + }() + case nil: break } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: FromRadio, rhs: FromRadio) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._id != rhs_storage._id {return false} - if _storage._payloadVariant != rhs_storage._payloadVariant {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.id != rhs.id {return false} + if lhs.payloadVariant != rhs.payloadVariant {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/README.md b/README.md index 1b8bed5a..f14d938b 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,16 @@ open Meshtastic.xcworkspace - Use Core Data for persistence ## Updating Protobufs: -- run: - ```bash - ./scripts/gen_protos.sh - ``` -- build, test, commit changes + +1. run +```bash +./scripts/gen_protos.sh +``` +2. Build, test, and commit the changes. + +## Release Process + +For more information on how a new release of Meshtastic is managed, please refer to [RELEASING.md](./RELEASING.md) ## License diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..087c3a3d --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,45 @@ +# Releasing Meshtastic + +This document outlines the process for preparing and making a release for Meshtastic. + +## Table of Contents + +1. [Branching Strategy](#branching-strategy) +2. [Preparing for a Release](#preparing-for-a-release) +3. [Creating a Release Branch](#creating-a-release-branch) +4. [Finalizing the Release](#finalizing-the-release) + +## Branching Strategy + +- **Main Branch (`main`)**: This is the main development branch where daily development occurs. +- **Release Branch (`X.YY.ZZ-release`)**: This branch is created from `main` for preparing a specific release version. + +## Preparing for a Release + +1. Ensure all desired features and fixes are merged into the `main` branch. +2. Update the version number in the relevant files. +3. Update the project documentation to reflect the upcoming release. + +## Creating a Release Branch + +1. Create a release branch from `main`. + ```sh + ./scripts/create-release-branch.sh + ``` + +## Finalizing the Release + +1. Perform final testing and quality checks on the `X.YY.ZZ-release` branch. + a. If any hotfix changes are required, merge those changes into `X.YY.ZZ-release`. + b. After merging these changes into the release branch, cherry-pick the changes onto `main`. +2. Once everything is ready, create a final tag for the release: + ```sh + git tag -a X.YY.ZZ -m "Release version X.Y.Z" + git push origin X.YY.ZZ + ``` + +Thank you for following the release process and helping to ensure the stability and quality of Meshtastic! + +--- + +Feel free to modify this template to better fit your project's specific needs. \ No newline at end of file diff --git a/protobufs b/protobufs index 0fd5023a..d191975e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 0fd5023a0aa67eefdf2292a624e8fbbda4489a6c +Subproject commit d191975ebc572527c6d9eec48d5b0a1e3331999f diff --git a/scripts/create-release-branch.sh b/scripts/create-release-branch.sh new file mode 100644 index 00000000..807ef077 --- /dev/null +++ b/scripts/create-release-branch.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Check if the release version number is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Set the release version number +RELEASE_VERSION=$1 + +# Check if the release branch already exists on the remote repository +if git ls-remote --exit-code --heads origin $RELEASE_BRANCH; then + echo "The branch $RELEASE_BRANCH already exists on the remote repository." + exit 1 +fi + +# Prompt the user for confirmation +echo "You are about to create and push the release branch ${RELEASE_VERSION}-release." +read -p "Are you sure you want to proceed? (Y/n): " confirmation + +# Check the user's response +if [[ ! "$confirmation" =~ ^[Yy]$ ]]; then + echo "Operation cancelled." + exit 0 +fi + +# Check out the main branch and pull the latest changes +git checkout main +git pull origin main + +# Create a new branch for the release +RELEASE_BRANCH="${RELEASE_VERSION}-release" +git checkout -b $RELEASE_BRANCH + +# Push the new release branch to the remote repository +git push origin $RELEASE_BRANCH + +echo "Release branch $RELEASE_BRANCH created and pushed successfully." From fff3b6e8df7fa25ffed9a00c8158a69c94c1fab7 Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Mon, 15 Jul 2024 20:42:47 -0500 Subject: [PATCH 002/333] Update App Icon to support iOS 18 dark & tinted mode --- .../AppIcon.appiconset/Contents.json | 24 ++++++++++++++++++ .../AppIcon.appiconset/logo-dark.png | Bin 0 -> 29213 bytes .../AppIcon.appiconset/logo-tinted.png | Bin 0 -> 22823 bytes 3 files changed, 24 insertions(+) create mode 100644 Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png create mode 100644 Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png diff --git a/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json index 937092d6..241cfb2b 100644 --- a/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Meshtastic/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -5,6 +5,30 @@ "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo-dark.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "logo-tinted.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { diff --git a/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9942a312f00dac9ba4fca00c0a5a438bfb50a099 GIT binary patch literal 29213 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 &v&@NbnS~xqbjq9ajx-hVn2Ourc`KfnFBjh zky$Z|78fOjYbdS1(jH@By|uZhMI}3Bd#vuXK^z`V0>FH~r(?^{fRa)&3PQ@*G}Gy& zkY!Wd(CFvFdV3VFh1crKJXd`5AAZoKqC6NseJl%8Sib{VFcJc9xCn1xGIIMq`kHB$ z7I<*%tbZ5hHt$nWz-q@Jinz1MRUEL71|zQFZ8?LaYU{DpT2-QqlsKu2frv zbi)pJg0L1-azC3isojB=qDEztpG}%nDzqc31v()y9V*bg{iycUCXey$)lT&=GJ1zo z!XCXTM2dW^0Qt>RJV8wSkWPH@&0AP(C3uAew(0H8`Z$?Eus)!kxH!J;vzcSQCfNI0 zdsv89a%Tb1B=R(J^V@$CvD&f^|LWbI8f|c>%+5h)li7ow#+YL5rF~VY4=!o#iK329 zKaM?Zki(w1c@ZqgoFh~`ZNA=QO_|35+|~W_yVxBiS(J%0OnUgIq+ncIsO-w$~je|{)C zkHNZ%jqKFRa}&xM>z6Dw(|E-tB)`oc7~dR(O}tJCB*syy=%a@@ti-IS(T!dFrHw{F zLUQ%P`-!lAdM%3piuDFIGmV`aAfwX9x;RS>RSz_1CkiWnSC{=eJjg1!&nX)*x zg#BA)w^Ou@ER1Y6fbbw~$008M1hliatm84qv6NYw*t=^1C;K;Qx6)8gSV-ylf6ibY zlfo*7s5U*pj!c=8*k`x=a~Gh<)Emi!7|;E#=iHu!u?+Bl+Ny1#>%R0U?REcexmJsn z-o^E~!*mP!Aq6QirNH4p;H+SQ+)GwWl^u?zC&?W+jnup>e*A4F(x)#LADmME5ttjJ znR$&uG?nr?(-?4l7E8O(xrOQd0`{k}SlPJk1j5kh7I2yjXfCZDfok5I*kkYoAUm={ z^FN5?t`RglgC{86Hc7P^CubvO&N)9FJyDvVoZ6zQ#zM(JR_n7}W z4(!Gh$D*B?towA%m^cJdQ**sCU0G(h6SycrIkw8S`Ak10MbA!A^=6K@sgtSHX#grQ z7l)^7;hhAvt6pb-#app|-(8rc=N(-vfNPH~Gk{mUtZ8;c8CyM|lB{+wjmyVCycgD9A(PS8_ zn^#Nd>NSP!Q!0(K9!cr%%G)?yOk(7_63Ez4K98|W7yW*06KnP zwGcl?*{UC}l~A#Zj)2Ud{@9!ra(r1hp=c-cgS~ffc<+8_nIz=ZYvEYj>`Um zS`uyXyOZq8vHynH0?a5U*J<1LD)Hg5!KTbXb`kS8HUfP%pP;O1EDU7X@OS~zO#9=A zAwEiU*j2{Y2^D%=g#)vzch>G&X@c0Kbq^h6-0`kW4i=|ee(@ueD^`1K z>A`BT?qBprmY!S!XUw1txbVCKxon*rRx!@5_1VJCJ|BeGp>8MIQOZ3~$&%^|C`Ig! z=ceQyxp43_artI}vNX~o49IdV7PB&elj*vGMhzw7e2~&E_E5t6V`te)*~9b=g_No+v&DGV?5gdUpL!+Q ztCijwU?&`;=L+TM8b}$XCTq_=pILn``}Rygl?v%xclc*DVd*;Kd`xGTi%G8K3NfGG zF!^)av@7%2NjRC=>nUbfWrlXbm+NS+aBg`YTq`&hQEm@~0*8ocqG$02I{IRnT|2P5 zTt=uj!c?|K0e}9vxonmz+Jv9;)?u8Oov~Ar8hQ;V+Vvco=?ZozS0B+R`V>YSDl6!j z2Bc3CCU?v>L3*`#&;)N^;r;hunxDf4H&a?HRbF)$$Daufwo zVe3#-a52V9HUgbvO>hVgw+Zyg>gSgCx1+cP9K@JXDX2s_VC%`Mx?wBGDwb^>FYwhg zfAebIi`<@1%+Aon&lym$#zFN32i()+!_J?fTnEbTJUe&gqFH<#RIU&bL{sfQK^iLx z3S~mKnz`2VvIa6`cGlrH_~0+>#Ow1xdw#Wi6--Q3G58RZ4-oLssK&phKavZyA(I+r z9s_By%#Nhu) zMWm!cwJouvHyx(Plmp%${KlkRX}RLA=s4p_aVRuEJl){k3FV{PmltqRzxzK%XU#6c zXu%a_R`k3`No7*ZvNdmHwf&DC^;HN|`kCzV)p~A_m>&_FJ-goXtWLxiCec1uihXFk z6%vCMfgqj(^ZI#0h9s?u0oXbHG%;^;WBvr(z4(i;4idoC=`turZL${^Y03tzEiGEQ$QR_W%6rvR97c8y<1Q*B#dakw+uiH_B9(kL>sY~MurZUOTsrximLD^nFShFQ_K z&pcE|4`|S0s1Fg2lA~IHx7YO+_Sjc8Slj3jnT^B%E@S7ox^1w>GJzQFEWc0wUL6}i z0qtG&Yx<5qI=KCIn8pZzhpbH>ffMV8;z>S0F2*>zE{azal8;aNu=jKS8$rEH4<}!- z1Es7{dsc>_m}r}_(Wn>Gq2e90$ro9%T#hp25idu`p3wzVHIhaO@eF%jW?}%uOb)#v z)+e~-3a;-=E1SSmuIr$%s>&8%XRhJ7Q&*i%3D^cX9``MF4vvuI14y|s4}g45$%Gm} zWA30~?y$K668~c(xV@U#V^-}0pzMk%I}j^RlqT|s&$3c3kDwyjxy1zrMws=GKJq3( zZde}9T?=KPc&l&={%c%=MQ=K=tC#`oQBR!17wt$BrMX?1jG%#QbnZy=24pk08_is* zJ$}Pd$XZnVP>b#CvU~dvIM1jhk})N8M+Z|+^my&J`AP<|FQO^YlDJA`5J6A!;Un08 z0srFT1(i7-6t#cCDCiw;7AvnadGMAq2dLPz_Sr3_^-2Xm%ljkEHdeHMr)~Zh!*R&P z3>z52KJMCvvyrNONKZIK-=71#Cr{HRP|hY(D){5@ngaX8t3xC+X?~JMHv^I$_C#(T z)aM5efJKMjMyDsN5c?bbv1*=hzy@h;GQRZv7XZX8?;V2JI?PXfMs6_>Y%Nk_C5D_|f2Do4p;%*_0Ws-nF-&!?I% zHKghyfI|C+YX|)^`pxA-FFXru#w#s z9GCReIAzCd+Gj?;yp`K~Zyv2*Tb*m`60d9^mu>d+^*NQV6XJ&D6>@KHHP-O>4Rf^b zMhpoOqjET*e#$a{-U3+{tlBLINALNlIs{6U8|*80-<`5E*zb*q^kb9MNUUN4r;W>u zuVM0;y#mneP=TRSwFFZYgfg_ZEFs^8n# zgcWnoc}6qur6rId2-&fsaRAa|-%^F}FkY}aeHOgLo0OFp_D^Bb!#W81#_TxtYW zjcKFRrn;=3A~{nWUrrV@dm1B~fR~JrDLpgYljTYXxLbO4U~jRku;7Ek;k3GD8vvUj zST?|I29V7cB`{whO7B9$e_yy4h(xhD&V;wgT6b^Z{6V_Z2Ujy$UvuRaitMH~q=$zT zS;|$4n?AT7Py%y?|B2VC;{%78euki^804ZQ^>}lbd|h0lo!-jXiHv~+ee@p^o%mgi zQjy~|fBBUl^p?Zi-J#9ty9-0-TK7u@nhHmY@H}NXhXS!WW2uXNsz05_t)Y%tR*4#B z=*<|yTm6WPJ|I!!k~p4PqC+`z&k6R;sL%0QisFltA7JEX91@3(nM6j#PGMAfODR3@NgHydDdhRQ-JYW zhd35v(HOR$YkOY^5%<35Vdd=T3$-hNbp2?kB}zpCkuwRk{JImjIi?o-u#$=CE~?S< z>0bT2Kh>0kP`sW0Q{sRqPYEY|uJ`(Y4@0W1Ehgx4o#`5s4=}&4rL8eYHdGkpv{i2y ztAsE|4j+9k1oFZ8!u{U?tlItS$1L;F277Dxlx5v1Xeq#UTw@>JHl6YSL``Mq1rVUg z@;SZM0pP{h)lO9^Dsn8s3IcRJ-{U}zcIQP^&?z#i2yGkq*d&yBqhbHEYSRVEt#Tove*`WvT7iLv~#fNQWeG+dIfwsGiqhAz;}O2M%a=-|6ZBlC$IpdbD}p4Ber`0mI5~z)0f%7I06?Hgf|kdb=gk8T7VDzmE4~ zUywMLUvI2@Mz~On{K&|ku!k~&e#vy0;Fv53LhhLaE?e{_0s(H$eWqMCK1^IU{=Gck z+pJ>;ASkM36WS;SD#cE0zJycn6+o}pXpg^y97(lEwRe_eyNinH?yLMGDheOm`&oK@q)VQ!@aWzu%UYrSo%e2Gei1 zN7sd(+M^19JE0ER6zB`#juQDPnp$%6ebU9}{WpGkYx3Z#N5+yKYq&LOjyIb0>Yv&bLxoAVX&f(8}PHH8k zk0zJJA!<#x*IBjV=vB)`BK+;%@ZvJvc8uf9^+MPZb^hRL%`{#51s^u+sf_HNLSNEp zw<6T2w%=M4uYPL6MERAB{fbbve5ZW^CgFP1!<23fhXdi)Hhu&jqfQ=fOMm_Ic#_K? z|Nm9e^k~8Hn)NG!&vm^}z6U~i$u&tpUC=FH@1rS7eR=K_}D#E7YV%vvZM4R1Xlk%p+ zFE3au-`1GUzCUvH2n$S;uz}T6{in~0_KhZ* zL)@=}Tm3LJiHiY&$l2%c;C<($eI2$+)ZIjF?NywVEb=XkrrRLG(%nJ!60936_~KoV zT$ykg^DD(gj-W1si+B=40_5GvfhRuS>TMZ6#V*yuxfBdO7snU znMP;&bl_z;C;P&-PD%NVI9Q?G)oM9}LRR+YqefS23)lJfunw|sZF!omsO>+aP zem_uALkcJLF!XjP7#$n3&j!0HvP;~boj!rsqkfiams>v~?9Nrjd@s&*xG=S3s{?FS zC{;1Awkc1<9N}&H?M{x%s+gq~=Zutt;$L?r$qo-e^k~fxq}aKKA*EK;NfRC1DlLV^ zZ~InXOp}lLy4_4^n|=ibpAi0bn2uT*9|u4;_c&UZqf7SP}$O@nhM-Qn;gK z2W!R)j6Q)#3a9XFG)pB%8MFURtH0f&I~!q&xGb1KA1s3nJP| z8(xN&F95(D=}oQTYR|tV-|k~`a@?c#=DVq?>C=V^@!R!pJyU-$4fy^EyN zEcDPco0tx~q28jyHKB2oR37$xvBX5=I~5aMwZctc-|2<@4sUyP7EhA!22$U-Fk5!}r*<98P#eLjTKkZ-iE>n{$RL z0doXze{1 z{-B~XJBV;%?Ty{s>;qc{`XhJJ=rZ(XQzU(0KkZSUzP3%D7`+o4rE3?PbU=-#{G1%5 z{?byC7LvpalH^oNk?n#-#4fqKUxLn9Uw7e!WU>TKFAm6Eq4s%ldFu}DTYwCULwPF; zLC$fG@=^XaC-b}b87qFr6{49Uf*zs0i`Jl0i2hkTW&L`;%exPfqNHzZwwC1Le?&;( z>K=yH?%p@(*c)Nl(S)2WJ}`D3-z@B)M$E`TlJpAoo{NwlxQM5mmKyHN@AxKOa`Gph z^0}J-OmX{#D|f36@^hqdb<4Deumt|+M;&0a(4ou~w)OVheihca{boZ-Z3lf#=*m5J z`EYY@mSmSx+>qpzC^8M;d*o^SY)*D&U#uQS(Ukd4%^xB2FOKAXjrq3H*TAYr2hP#O z^_~h-mcb#)U(@MB;?BoXzEB}QC0*IKcflTi2~3o3mXT4DsQXp(I=cK@K5f@n6WT<~ z*kTI*;`_VLR z&Wpv*#@EO!Y(Ff_7&YBGvAnHWOmTCk)4M=^ihfMxgU-llr;`vG7GJCSrAXgqF>m(Uu5cYE%1FaK1 zVstbOxcp?AG`H~sc}}QWC696L(yaX>HE%_fVr=l7j~_V5o+S4|maooaem?3wh|4_8=h=4!yepl%t{;m%dnc+`)ZZX8JA{(PFn zL!Ys~GkqFShD)Z`+t>W>^E^>&%5c3_oGt#jHAu!ADsaHI1#aOLWRkUvNYW!WH>F;sm67_WE^r;l%6r0reoh+U5=^hA!oyb@&==HA8t+Mtr%=e^j?%P;<%m0}^roS; zp*?t3nr-KigY0D)k((jf^9u4K0c}1;`Qj7f>*f&2n~msUzg2eEiZgv45=V(YS6^xS z_N%>t!}06M%I^Ip8T6qJ`cO+w%7~1TSu`t-;F^ULR-hnGS@Mn8hDduOPIi#wVoR05 z4|>bjngGGs%9TIQPXf}FdDCPRN3KCVyiO3l-b8YcKHR_88=?Uj(U%h{GaEMj3!)qd z*DvhCItEr-*Y^Q^Q&(Dgy6@8jL6xwwS(DUQF=1?c7)t8lzdSE0KjPnZVtl`&>B)k5 zB0sY01an3t}(JN)FClcwFjJwn$?rNu8 zlSdOJyu@Mp7ku|Q2sK#LU+%|f?h3{y~8-w`+1zhEaAmz#PECo7Ofp;7B$|Ec@$2#aA>c(eXXeK-4 zPw~~$#~aV@2ZZajvXjM>&c0gFnCPK`gy@`LZUB_J^hg2S00%`ck9}_sxcpmI?X0eM6<^e=Un2U1zf=sH6^%E zRxhUc@hA$!co(qkti zecO8k>6e={A{zv-u4pB|zP2g*D0z(8fI8Az6> z&JS0XEC9W+xSf>BDPq!g4Bz!Riwwmx{9%y7njQI#+78G5x_TVBEcdvi>{iN1AOPe| z%eZP_Pj9k2-&eLK|HNU%i!j5AGt%z;tVjhDmX*N)vu9tgcmj~qRx=5H}jL( zdd>%b7MA(hKG?|HLKkF|P4914Gn9c1KX|R{3XZKT5>OqXqbErMz2MF53FCD?{MvUo zu<_EBLNcCN(F;JYW<*pgznC!k0lhkyX&8_^OHaaL`DvJSCc1}AYN6f&h;_*3L>8=X2_l)?#@r-n5|)XN1#Nj0az zbz17`?!kYD+S`gJLNAw(Z|$7j94IeNT75CK_| zS`Q@Tlygkn7KtCJT61W^-8oVK`SwN{AFv^t zq4cejiH5^bFUEyG+L$qSwtt6mDy%mmK6KConf(ql5VOW@GBPT2C>7ejLes6Q(W@TH z-~+v!)lN?~_8*eVU>r&sr31YcS9>{&I1T!mkI2{St6PV>ULqGGrgvi~nQ$L(HPC`Mnl`AopL0 zyVLxCAZ{A#Vfnk>Uh*Syi%q@(1cNux8`k?C2j~jF$!{Dv_J-R}=kD;enK8hG$+B{7 zF3+pLTK-U9_-id@+MoT}B2?eW`2Dsf;?OQsazHJAQ^ue`_nCO23&lb&=c5jf(9Y7}oF0S@tYtp#=i z+5739j-ExyiYnOmd-%jUbWexR-F%$1eNo;PZR(sV3dQl?x;sLF9Y*83FQ8l*Nu9Vq~7h3?O(lDc?xHcyWwd)QCMfa&|$-9FI zwZ`1mi_jrouhlNdU~v}~g${WE(8^?S_J|pL(|1Y0+3Y8+ee>8nD{-wK_VS-V%O)dY z_&p5}OH$Y%$H8F6xk_pKmLgG?YVe|NBQcGtjZta6)C9@!0^X#MSOCRkhz&`EI&QQg}&2<0d@-h2Ms4-jPw zBGu^^1{(cJy??;gaX=Qz7pf#wG$mH2Fq&{Cc0He+Q8n{cBK`SM*#j?sDK_* zUkxCyxH;(wJ@9lWpMj1VM>nbW*FT$>*Qeye=b1%-GE1}WoWAbGsEK#}aZ&yN-F)P* z7IpSsJT!i8fc7!Z@@M_>hbZI~h!^>b=Q8LgP$tIVK5H4a^ESyoLFbQ{CVrg;cCxn` zmClNBIT34eQ14ol$cj%dDs50IjoyJ;Yi`6+{c)m^fzP;e3J@0y>f$%?F3mj~c^(i1 z$FdhtpoI?~k-KWFx#^hoLO-z`DPK4b5gip<>gquHwX2MFkEb!zT}3s0G0NR3-*GI& zy}8Kk3IvfV4TDp#mPbxf5xA3>$w9X@p}#)a@f#B5Ew65IUy`{$N4ogvQ{ueRx)Je! zivEZICjnx*?k55h?v+s;*byyDHf8zyH?s5=%sk{~^mirGhssJe$(Cw$S57Kfg$Mxn z*0rz^tzDcvWgSj@&4j?X(t#zUK|{`n_C?CQeqp5GGl$eR+qu`fA&0P5R+O-I6Il1; zhw3L2x6R6~f_Rekm^!*dyzIy$bvU3P6+mFNV(8>PmB?tj<)h3k8~XPbk=ano$G5Gr zG|fUumy%E(7CHl*>}ItAJi$OuB7o+EY#(0*k(!V3{-+UUHtT-1AvYEXT2+R0o@L$V zA=>ESe9_SsDy#c0j(Ftr4svnes~cL0Hq^uAS4Bx52{q~uAgh8j8xd!H@0H77`CVz-U@Js9k0$AHjOtYiBgdfe)HNa@;BSH}9>>BjBfOsC_}K z2o36OZyzGRieGI0Nv3OdyUdI=AYKRETIM&Du55^LmXSWX2;LL0KA)O_@?M{@&VB87 zYmq3ZHSdjw#G#r}E9V|3X9I`>Ey@^7kwnbSk3rVt?b)xZ+E=tG*3yFJ7;eHl4srxk z-Tjh^Wq^W;m)xK&7ebE!NMyer)OQu#0fWWNA$ky|JZPaCzBLJgXF|zZl=n-GRVU`l z$B~;MUG^W!J`JTG0-JSTf8P3cw2C^v*MGt>K^j9IR9uw0({vl*w*acwtGMj~$hJzK z-0_SGN;Ue`GWj^I2c54VUd#p_aE5~X3T04egp;FN<3=gppwknrUf=1n$f??oer0zj zntJ7Ffq8opt1u`L1{p3}H$wK1TFc%ds&^+WB5_}2y>jR;~ovLgGq=jc}CPjpvQ#sCJ|F7trdtL9V zbCPoBh%~4#YPQKLJ0Rv>hTDuGrXi-UU+=JTgPLVrl-Q$HCXD(~+V41vBbe1V(et-% zjGR&fd7JB-sC+7f=Z7+fM}MQ;iPMOjI$-pJl*8wv*DqhheH)(nt81~-(I1qt+G|;d(y=&)i?0PRaB1U4}w8`yWCx3!YLdnd_ zXBWf`{D2p%rSU{t6+RDX3$QuK7sqMfx|EQ8-?s7C^#+&j=&9FPbFAVpI$`0TAQKiM zPueDupZkn>{SXtVa4AnhTlqJ0idRH2Hi7Ue*Y|lVS6}O+1%KfsT5&*f?BvU}`*y%L zgAz)QGU@T%JAWo!M=o=qLGopn0;*$d3%3p~^;*0?borsOv+b|B`ep6@A>TKnEMC~P zCQAlE^PpNPAhu))IHda!xyjd*&eLQk$->dEX3$z#sXx7M_P0hs@Q~{ifu8;@$7&%E z<;2@aQX^}8ms4?42Vkg~P^s;=+8s6bozv%P5ibY-AHonW&<5Yx*3Hr!hHSV)yl1># zuC)Cby2O9caJj+#Phk}DMS?$3j&|gN9!bkF2+w!r9S7%1p`bEupeXFhfs zl@6)qfe*zkU)}b%L!y4qy0^{zngIE1T&+WstxpNh3mr$eoKU?Yh6B`=1W;Qh%UnkC zxkr4#nTKSVUy>z_8#L_1rmPI8K@KO7JIhZ8nPx!!|CN8Rz|?6m9dJR27cu;3i6M58 z%eSMB>P*e}yz&u#QKmLkye{t{4}XoTPX7tm$Q=!oz*S&io+Vt4FYO`xq`LuX$!rJ7u zvZ%^eFxcEMO9iP~@LT^WW-hnX$~~dRc!;0|2`wJ=a}q6NN@?A;;fK6=GX1oJl_(va z1aKW~Y3=4hLT5!-+wV<2cb^^CK6#4#AhrIF`xf^-^#~jzRf7Kl?W@d%y-gn=Azqad zvM)Brzaqd_QdKy^3Be!Gb2A;G)el?Bpv=36`muB+SUi?ZaTlSQ8+Ipz2T%WEsIcX< z=Z2&I%LJk-P>T=-!y~7w1Iqr^5o_xxww0v`h%WsB{$@j8KrM+clkJ>EXBRo@?Cxx3 zN1Xof-Nnkc`?vXwP5v!BBTM(^Q2{C>E9{Wbe^08zo7EVGVdsf1|r zpZU09wM&o6%&1n(rvuH)=j6ThwkAACEKh_iUUZH`uLVQ>k<58n1gZ`H3lij%%)bU$ z6Ly2S1;%r=|B^0XL`276YpqNS^F}WI+5Li9SYdwz71VbWnMrv9vJsS0Up)I$V*T0% zrAHXf`22@0eff+8{y$3o4cktlAe9H}RNd_y2ghi)?tLxvw#5KzaVoonbiJ3$kH9)u2Au8-E=s% zGOK+3lclQOM8}^(LfD*OH<IzNbjy(|vV-62Q0Dy__xW6AwzwakDFVXe~w6 zIK7x_}%tz z{8p_LwEH)OM+}RUEGl$5;l;lSFAT0N?cZl~tQA9FGxP3ySg)ds7W~HEea30Z&PMXK z8ns1c*#OGA|4rqQH>Y+^X-JrKqQh!+bK~>nK#Wr1ODC&$Co~sSBUyK8X#O{L+s{1z zshl`!Hw8%`a`y`F?HkKO!T~4^!i*NE2%>z96qyX;bkK-4$9ki@mVI(SpU1%y?MXUQ ztJHDITfs4EO>RF2gUB08Mu)yyfv@F#ukk<0?85f#| zY+k0URMw{x->mt8$~u&tgAH8_YD2tEkVQDg-ZP_R1-0sgTIvv)QvUR>=yzWXho>19 zJWHd80dJ?o@ zefsbX`s@_l5hl)9IaNjIw(a#QZTrI)rO;8myQN0CDLRl96YCJod-oH)rCi zF3P6jBm>6yHgYZJ{jNqM7KTi`GZEp!GDUE;K=~yJqGUZdoxRG}PR{=Q`2*c5XTaEC z05za{D1=|_mbKGmz6Pp|7vula-j)ADwg3M^xAe)4?k$wk)I}TF$N{l6YWN0xMG8p?hpVygB-S7S3{srGh^TQaNIcLuMyx-5) z^Ywf_XC`s=FS<8nHndg9{hP2_U6zJ`CC7^5uZX?l-}qgdq5%i=hU?vKmNoF=Hl|+M zK{w~LAd0=VFQi^5@|29#c;DVuD%SU8A5<6h<-ClP2t|tgiq~A*IYaq1;BK zj5Eqe{i4Qz(p3z$Q|I$Zgb(#luOYmI0c$Nb0}{0n-6 zPL9}RnpAErrHzTGK_i~r*~e??yHM-_Ju`hf^4{~*qtS_1o+p3?x8EA?xmK8#m8A4k zaFx8b2+t<>W+njb7vg}D>$ z)wwB!45TD*F`ICX_Ge_;^0lO~7bgT+rk)%2zzf&;o<3k*ihbNuW-eK=uk#k}JsYQ` zdzzs(4VW?)>b;>lRUbVqd3J+{s4ww>E|qptjfVA1)B!$d@jZQ@ELkQBDDc?|BIFg_@s z`JGo^k_JNnnD3gL-19 z^X*|&R1ZR79(4{8{udy(xv+ukOKJ&JHW0crp~`JZ{f8;`uxIl+F`p{_I1W9!F_uWu z+Z7Qp>QHM9^iQh%?S+qGVGun6d?opM4#1+;-ntH{0UpitL6rh%h`&HvaF9jh)3;AA z7B_3|Gt_NyH)a3>n=KQ|dsT~6L`Ww} zZcCG^*i4%3ROT?^nZio@Whp!0HSom6THqdC89<@D@T0=!34bq7t~OY5N+o{RlD{jm zGo?OuHip2%sq=za{nl-gD8_Wpp^i9UqUI@v>Ec{nq3Q(iwuLgeBI0YP0=k|X+g`(N zI@Nkvxaj{Q(yCOu{`a>}y;&zIL$5@O$`5{y92^BVB`=JCPE8&1%L@0E(Gi|x$S!%L zs-DG$(R%jk)aS-B0y?W~%&Q=ZrN@>&h%`U|bOpZ#=0-MQhpxV24qn~eU5680K6bYpi$1l$OWk*jEzO%>Wg`x1UkJHgvfO~` zPl^9BBn*`SO;+3=J~jVjSGRVsp%?f8$XX|4Ozv7NeMAAFKmLhhAs6$q3tY&qxVhp` z-L-5eTN118m8PX=L2bUU%C7_AlizAZpn%xRRF1|;72(5nL!aFiQ!Z2n`KyeoFaZy+ zFN!ij88FM8g}HQxS=<$Y@z>m%`LN^SSSsYL{?<>BYVx@;@zwCbg9Dpb6?OcQ`io~a zHp*;38e_yTJ?(VV8YAK-R9wH>L*JR=R}oNEZWJJ5XMbi{5tgD{fqYO>iSE)f*k!;u z!=$Cmkxk-yzamlcThq+vPhEgYT$_C3hu)De8OPQ;>V9B3GWiw!;Ff)Ux03I|6}VE` z^BN6s1oOPEK?m^+*n2~5d{F)Mctf1d{0+aCQb|y%{4gjM@UCVZ;*A;W1k^{UdeaJW)bIFau(*Ws}0FYFTwgCrl=ZH_IiqYp-S z#CO5DpP`-f4^KVxV1GW70ty5W- zFxrMefvDXajc>eA$(MrJbMl7{5T=&Xp??H6n}^zMtXFuR#0c+z5W|%}T5vFhOc9tH zVxk1_vmWST%vqm4eHr0tB*0nx#w%pG${8N87Z|^Zfu~pBb}(E^@SfeJVMNpfC0~D@ zG^`7fKwz5d!1bM4P0mRoFbyYZ*|k^#hz`cBd!tw|j>3*&x~#|g3*44VexAC4?bqY; z%8esKAr{d-{_Vq(TjlBNr22-i-NB4#Yphy-Pmh2QxE@MCnj}TCw7eS=|KmNbcyZ*~ zbr!Qh<7A!HC8_ysT5^s2;n{uZ-sLL_qReMnvd(#4L~p<(!&FoNm%&7Mnt4)oZKq7H zq_8nuh!cr2a|#W(OjkO*pDa$dOES0ZDWMgE&rRJ53qD8%kxH^)0#Pr~lYBbk-Ktj9 zJX_n^91;pvfrgIr3HlBBAmKcPb}t?7B^SOU)aqIdL|pHmIaAyFx|tg4rGpPs!Z#tv zZ+Fn|2>4xMO?v06bx09fS0F;s|2dae+Y!!m{eH`y;ISZvD@11uPqa*pw$Q?XJqu`+ zG|GASJG}hh-V;~mm$+uN5qW&pp~J>;{XtD0={iVqwTGb@?5lC)_~rhfTle!wl9OvzOt0Tz-!Ns8OYQU8Muv_EZRd7oJKCK@)6@6@qYX>IOMWRD(LhsrG^Y@6YU64mvD^Kh!dphO=2 zc+xx0TiLbzgweq=nqIWiF_I}1UJm5mncW#0h75uvJ$8hc@Rucllx~Cj8XwZc+C{x7 z=gx)HoKW$}rHa8ArhDFm>zUKk@t!BR&g7!|YKXiLj{(>R;bSBS4r+tdWLK;Qb! zlUucDTr4_3rHnSf)X3yJLEa3#`#n;`A@P)4~FWk2gr)V(4|ODx_E z6;O5^JxY+A6@( zrdB`D*K!JKbB%F`OV+ySafhE2irvu%pFez*9iS!r9wkN36&fGFEU%YXYkdf-K%XK4 zSWlH;MJ3z)V$YUiID?v2YW0z(U9oOTaYkoPayIMA|hB}l?Qpn07huYr9l-eV1aV_yl{4R&zT zx?{4%=>?^$qgA271f)7#Dc^B5zj25upeT{iKN=p1n+mn96GEZ{QmLEWo+XQVOo~qH zt@W-QrA>QGk^--1{Q#++Y=Sg&77AW;begm*b#o$(zMu>7TE>hYi@hr{W*hs~?=^c* zj1Bp0WGZ6ujeAL~-AKp={jWNv=84HOhw|hCvlXM#J_WZaUPRHj|FXHr>U6e0N4q6@ zh3aZGhPE=0>WcQXs;YlzTm zizcTqd3+7sjWxr-mh;=*Rp&(wCp^6(JD;KcEfrT;gqv^l-zRkh@ZA-U86%5O3ROfb zm2Y8G_7+i4Nyctc_QsWnbJ*mQYZvSL_h;DLQJ>q1E_!LGxu32^2$BNA56MoMNvxLE zD?mGv7`>osm5a(U>>7kRgI1|ya>4>T1FUmSUuU(mcI|PdCap@gFt46&^GF!dl5XHY z;)$rWKNCv~#}a3X48L0MR_Ia53NrFfEeiyZ-gFC^(z=#=%*TgyiPbQ5w2s2)pcO6d zl70NpiFd%OIcZ$ir)kC@ZaiAn76VO6|B)gaB(P(J(((ZN-fXqz9ji>9a^xHRgR1ZF ze-Mw`$B=?Ou+Fg-KBI#vjNZKmHG)GG14wDZ&ZL| zeUFIsi1u+*=C)m|T$*wX6O3GWQ^y^5gjuwg{|`_w?Vo<`-AR3E*fxrp)-ot76zJxr z4#ZL<_Sm5D1gB?;mAY*ctaO4)uH++r6^XB0l1TN{1H2{!Vnw`b_Xsoi`YwP3a>Dch zs}Q#h2>(m4?}oo_b&hP9-LJvqjj9X|CRn+kjnRwq2gsH#Chz2#$5;zjTzX(^fy~Cv=2yxQ~d0f?-xY6O@#)9s34CYGTl~j(I%q!6$)!6VKj(2X7S}2vat;fL5Nev z!dQ0zSRG>m(|T4twc1 zU#DL+*vBPxORo@@)IZ+#Y!na)op3`(QI6}Uy8N+sw5@)G-zpcHU8Q|yhDj`?*ioqa z$R1Jn@G$fy5_k;ZKp$xT?~4WC4HZ`CudoTn-Zc`MGv}LIH|~KjHCTOGY4z8X~k29iWZOlHG$f@Gy{rU1HX8qJQ1+ z3wr-6dyDN`(8p4H(0eOQ^u9U}z5n@ypQH5uXKHL|vKiw#pY`Q4I<<997#z<%YIXC! D31RPq literal 0 HcmV?d00001 diff --git a/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png new file mode 100644 index 0000000000000000000000000000000000000000..8591564379d0de5908f89c2e2f6d0a1b45d7f4f8 GIT binary patch literal 22823 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 Date: Mon, 22 Jul 2024 18:29:22 -0700 Subject: [PATCH 003/333] Identify repeaters in trace routes, dont add them as a node in the app --- 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 5d6cceb8..93879e02 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -827,7 +827,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var hopNodes: [TraceRouteHopEntity] = [] for node in routingMessage.route { var hopNode = getNodeInfo(id: Int64(node), context: context) - if hopNode == nil && hopNode?.num ?? 0 > 0 { + if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) @@ -852,7 +852,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) } - routeString += "\(hopNode?.user?.longName ?? "unknown".localized) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") --> " + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") --> " } routeString += traceRoute?.node?.user?.longName ?? "unknown".localized traceRoute?.routeText = routeString From 9f28a1939a4dc6fa4868da6f5eb31e46d03626ed Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 23 Jul 2024 15:09:26 -0700 Subject: [PATCH 004/333] Add chart selection line --- Localizable.xcstrings | 3 + Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 128 +++++++++++------- 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 01c0c90f..d0857465 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -14631,6 +14631,9 @@ }, "Mininum time between detection broadcasts. Default is 45 seconds." : { + }, + "Minute" : { + }, "mode" : { "localizations" : { diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 51a68a49..d62d62c0 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -24,6 +24,7 @@ struct DeviceMetricsLog: View { @ObservedObject var node: NodeInfoEntity @State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)] @State private var selection: TelemetryEntity.ID? + @State private var chartSelection: Date? var body: some View { VStack { @@ -35,60 +36,79 @@ struct DeviceMetricsLog: View { .sorted { $0.time! < $1.time! } if chartData.count > 0 { GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { - Chart { - ForEach(chartData, id: \.self) { point in - Plot { - LineMark( - x: .value("x", point.time!), - y: .value("y", point.batteryLevel) - ) + if #available(iOS 17.0, macOS 14.0, *) { + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + LineMark( + x: .value("x", point.time!), + y: .value("y", point.batteryLevel) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") + .foregroundStyle(batteryChartColor) + .interpolationMethod(.linear) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.channelUtilization) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") + .foregroundStyle(channelUtilizationChartColor) + if let chartSelection { + RuleMark(x: .value("Minute", chartSelection, unit: .minute)) + .foregroundStyle(.tertiary.opacity(0.5)) +// .annotation( +// position: .automatic, +// overflowResolution: .init(x: .fit, y: .disabled) +// ) { +// ZStack { +// Text("\(getTelemetry(for: chartSelection))") +// } +// .padding() +// .background { +// RoundedRectangle(cornerRadius: 4) +// .foregroundStyle(Color.accentColor.opacity(0.2)) +// } +// } + } + RuleMark(y: .value("Network Status Orange", 25)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.orange) + RuleMark(y: .value("Network Status Red", 50)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.red) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.airUtilTx) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") + .foregroundStyle(airtimeChartColor) } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") - .foregroundStyle(batteryChartColor) - .interpolationMethod(.linear) - - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.channelUtilization) - ) - .symbolSize(25) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") - .foregroundStyle(channelUtilizationChartColor) - - RuleMark(y: .value("Network Status Orange", 25)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(.orange) - RuleMark(y: .value("Network Status Red", 50)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(.red) - - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.airUtilTx) - ) - .symbolSize(25) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") - .foregroundStyle(airtimeChartColor) } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartXSelection(value: $chartSelection) + .chartYScale(domain: 0...100) + .chartForegroundStyleScale([ + idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, + "Channel Utilization": channelUtilizationChartColor, + "Airtime": airtimeChartColor + ]) + .chartLegend(position: .automatic, alignment: .bottom) + } else { + // Fallback on earlier versions } - .chartXAxis(content: { - AxisMarks(position: .top) - }) - .chartXAxis(.automatic) - .chartYScale(domain: 0...100) - .chartForegroundStyleScale([ - idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, - "Channel Utilization": channelUtilizationChartColor, - "Airtime": airtimeChartColor - ]) - .chartLegend(position: .automatic, alignment: .bottom) } .frame(minHeight: 240) } @@ -191,6 +211,12 @@ struct DeviceMetricsLog: View { .padding(.bottom) .padding(.trailing) } + .onChange(of: selection) { newSelection in + guard let metrics = deviceMetrics.first(where: { $0.id == newSelection }) else { + return + } + chartSelection = metrics.time + } } else { if #available (iOS 17, *) { ContentUnavailableView("No Device Metrics", systemImage: "slash.circle") From 53d5f4bc7c12cfcf62f728c0d774913ae7ff29dc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 29 Jul 2024 20:43:01 -0700 Subject: [PATCH 005/333] Add debug to meshlog and appdata views --- Meshtastic/Views/Settings/Settings.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index faad71ad..53bc5eb2 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -469,13 +469,17 @@ struct Settings: View { case .telemetry: TelemetryConfig(node: node) case .meshLog: +#if DEBUG MeshLog() +#endif case .debugLogs: if #available(iOS 17.4, *) { AppLog() } case .appFiles: +#if DEBUG AppData() +#endif case .firmwareUpdates: Firmware(node: node) } From 447d82030af77562f438eeedf83cd812b209be2f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Jul 2024 13:48:08 -0700 Subject: [PATCH 006/333] Log Cleanup --- Localizable.xcstrings | 3 - Meshtastic/Helpers/MeshPackets.swift | 2 +- Meshtastic/Views/Settings/AppLog.swift | 147 +++++++++++++++-------- Meshtastic/Views/Settings/MeshLog.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 25 ++-- 5 files changed, 106 insertions(+), 73 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d0857465..a2953404 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5052,9 +5052,6 @@ }, "Debug Log" : { - }, - "Debug Logs" : { - }, "Debug Logs%@" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 5ee37673..0d6f5917 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -670,7 +670,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.voltage = telemetryMessage.deviceMetrics.voltage telemetry.uptimeSeconds = Int32(telemetryMessage.deviceMetrics.uptimeSeconds) telemetry.metricsType = 0 - Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx, privacy: .public) for Node: \(packet.from.toHex(), privacy: .public))") + Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx, privacy: .public) for Node: \(packet.from.toHex(), privacy: .public)") } else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { // Environment Metrics telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index 2ed2b081..2ade781c 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -9,7 +9,7 @@ import SwiftUI import OSLog /// Needed for TableColumnForEach -@available(iOS 17.4, macOS 14.4, *) +@available(iOS 17.0, macOS 14.0, *) struct AppLog: View { @State private var logs: [OSLogEntryLog] = [] @@ -33,60 +33,101 @@ struct AppLog: View { .secondFraction(.fractional(3)) var body: some View { - - Table(logs, selection: $selection, sortOrder: $sortOrder) { - if idiom != .phone { - TableColumn("log.time") { value in - Text(value.date.formatted(dateFormatStyle)) - } - .width(min: 125, max: 150) - TableColumn("log.level") { value in - Text(value.level.description) - .foregroundStyle(value.level.color) - } - .width(min: 85, max: 110) - TableColumn("log.category", value: \.category) - .width(min: 80, max: 130) - } - TableColumn("log.message", value: \.composedMessage) { value in - Text(value.composedMessage) - .foregroundStyle(value.level.color) - .font(idiom == .phone ? .caption : .body) - } - .width(ideal: 200, max: .infinity) - } - .monospaced() - - .safeAreaInset(edge: .bottom, alignment: .trailing) { - HStack { - Button(action: { - withAnimation { - isEditingFilters = !isEditingFilters + HStack { + + if idiom == .phone { + Table(logs, selection: $selection, sortOrder: $sortOrder) { + TableColumn("log.message", value: \.composedMessage) { value in + Text(value.composedMessage) + .foregroundStyle(value.level.color) + .font(idiom == .phone ? .caption : .body) } - }) { - Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") - .padding(.vertical, 5) + .width(ideal: 200, max: .infinity) + } + .monospaced() + .safeAreaInset(edge: .bottom, alignment: .trailing) { + HStack { + Button(action: { + withAnimation { + isEditingFilters = !isEditingFilters + } + }) { + Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) + .padding(.trailing, 5) + .searchable(text: $searchText, placement: .navigationBarDrawer, prompt: "Search") + .disabled(selection != nil) + .overlay { + if logs.isEmpty { + ContentUnavailableView("No Logs Available", systemImage: "scroll") + } + } + .refreshable { + await logs = searchAppLogs() + logs.sort(using: sortOrder) + } + } else { + Table(logs, selection: $selection, sortOrder: $sortOrder) { + TableColumn("log.time") { value in + Text(value.date.formatted(dateFormatStyle)) + } + .width(min: 125, max: 150) + TableColumn("log.level") { value in + Text(value.level.description) + .foregroundStyle(value.level.color) + } + .width(min: 85, max: 110) + TableColumn("log.category", value: \.category) + .width(min: 80, max: 130) + TableColumn("log.message", value: \.composedMessage) { value in + Text(value.composedMessage) + .foregroundStyle(value.level.color) + .font(idiom == .phone ? .caption : .body) + } + .width(ideal: 200, max: .infinity) + } + .monospaced() + .safeAreaInset(edge: .bottom, alignment: .trailing) { + HStack { + Button(action: { + withAnimation { + isEditingFilters = !isEditingFilters + } + }) { + Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) + .padding(.trailing, 5) + .searchable(text: $searchText, placement: .navigationBarDrawer, prompt: "Search") + .disabled(selection != nil) + .overlay { + if logs.isEmpty { + ContentUnavailableView("No Logs Available", systemImage: "scroll") + } + } + .refreshable { + await logs = searchAppLogs() + logs.sort(using: sortOrder) } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - } - .controlSize(.regular) - .padding(5) - } - .padding(.bottom, 5) - .padding(.trailing, 5) - .searchable(text: $searchText, placement: .navigationBarDrawer, prompt: "Search") - .disabled(selection != nil) - .overlay { - if logs.isEmpty { - ContentUnavailableView("No Logs Available", systemImage: "scroll") - } - } - .refreshable { - await logs = searchAppLogs() - logs.sort(using: sortOrder) } .onChange(of: sortOrder) { _, sortOrder in withAnimation { @@ -176,7 +217,7 @@ struct AppLog: View { } } -@available(iOS 17.4, macOS 14.4, *) +@available(iOS 17.0, macOS 14.0, *) extension AppLog { @MainActor private func searchAppLogs() async -> [OSLogEntryLog] { diff --git a/Meshtastic/Views/Settings/MeshLog.swift b/Meshtastic/Views/Settings/MeshLog.swift index da2b5014..e63c2f34 100644 --- a/Meshtastic/Views/Settings/MeshLog.swift +++ b/Meshtastic/Views/Settings/MeshLog.swift @@ -18,7 +18,7 @@ struct MeshLog: View { let url = logFile! logs.removeAll() var lineCount = 0 - let lineLimit = 1000 + let lineLimit = 10000 // Get the number of lines for try await _ in url.lines { lineCount += 1 diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 53bc5eb2..414daf0f 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -243,20 +243,12 @@ struct Settings: View { var loggingSection: some View { Section(header: Text("logging")) { - NavigationLink(value: SettingsNavigationState.meshLog) { - Label { - Text("mesh.log") - } icon: { - Image(systemName: "list.bullet.rectangle") - } - } - - if #available (iOS 17.4, *) { + if #available (iOS 17.0, *) { NavigationLink(value: SettingsNavigationState.debugLogs) { Label { - Text("Debug Logs") + Text("Logs") } icon: { - Image(systemName: "stethoscope") + Image(systemName: "scroll") } } } @@ -265,6 +257,13 @@ struct Settings: View { var developersSection: some View { Section(header: Text("Developers")) { + NavigationLink(value: SettingsNavigationState.meshLog) { + Label { + Text("mesh.log") + } icon: { + Image(systemName: "list.bullet.rectangle") + } + } NavigationLink(value: SettingsNavigationState.appFiles) { Label { Text("App Files") @@ -469,17 +468,13 @@ struct Settings: View { case .telemetry: TelemetryConfig(node: node) case .meshLog: -#if DEBUG MeshLog() -#endif case .debugLogs: if #available(iOS 17.4, *) { AppLog() } case .appFiles: -#if DEBUG AppData() -#endif case .firmwareUpdates: Firmware(node: node) } From ae5e0c946e461f5f1f66b797edd801d92c6f8c0b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Jul 2024 19:13:09 -0700 Subject: [PATCH 007/333] remove conditional fonts --- Meshtastic/Views/Settings/AppLog.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index 2ade781c..0a2aaad0 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -34,13 +34,13 @@ struct AppLog: View { var body: some View { HStack { - + if idiom == .phone { Table(logs, selection: $selection, sortOrder: $sortOrder) { TableColumn("log.message", value: \.composedMessage) { value in Text(value.composedMessage) .foregroundStyle(value.level.color) - .font(idiom == .phone ? .caption : .body) + .font(.caption) } .width(ideal: 200, max: .infinity) } @@ -92,7 +92,7 @@ struct AppLog: View { TableColumn("log.message", value: \.composedMessage) { value in Text(value.composedMessage) .foregroundStyle(value.level.color) - .font(idiom == .phone ? .caption : .body) + .font(.body) } .width(ideal: 200, max: .infinity) } From a9e189be9afa0e4105af035b97b21195ef3871cf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Jul 2024 20:57:36 -0700 Subject: [PATCH 008/333] Dismiss selected node after deleting it Dont allow deleting the connected node Hide 3 context menu items that need refactoring. --- Localizable.xcstrings | 3 + .../Helpers/Actions/DeleteNodeButton.swift | 77 +++++++++++-------- Meshtastic/Views/Nodes/NodeList.swift | 32 ++++---- 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d0857465..fa57ae2f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5196,6 +5196,9 @@ }, "Delete Node" : { + }, + "Delete Node?" : { + }, "Description" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index 247a4380..71c3a317 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -10,42 +10,55 @@ struct DeleteNodeButton: View { var connectedNode: NodeInfoEntity var node: NodeInfoEntity + + @Environment(\.dismiss) private var dismiss - @State - private var isPresentingAlert = false + @State private var isPresentingAlert = false - var body: some View { - Button(role: .destructive) { - isPresentingAlert = true - } label: { - Label { - Text("Delete Node") - } icon: { - Image(systemName: "trash") - .symbolRenderingMode(.multicolor) - } - } - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingAlert, - titleVisibility: .visible - ) { - Button("Delete Node", role: .destructive) { - guard let deleteNode = getNodeInfo( - id: node.num, - context: context - ) else { - Logger.data.error("Unable to find node info to delete node \(node.num)") - return + var body: some View { + if node.num != connectedNode.num { + Button(role: .destructive) { + isPresentingAlert = true + } label: { + Label { + Text("Delete Node") + } icon: { + Image(systemName: "trash") + .symbolRenderingMode(.multicolor) } - let success = bleManager.removeNode( - node: deleteNode, - connectedNodeNum: connectedNode.num - ) - if !success { - Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized)") + } + .alert( + "are.you.sure", + isPresented: $isPresentingAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Delete Node?") + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingAlert, + titleVisibility: .visible + ) { + Button("Delete Node", role: .destructive) { + guard let deleteNode = getNodeInfo( + id: node.num, + context: context + ) else { + Logger.data.error("Unable to find node info to delete node \(node.num)") + return + } + let success = bleManager.removeNode( + node: deleteNode, + connectedNodeNum: connectedNode.num + ) + if !success { + Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized)") + } else { + dismiss() + } } } } - } + } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 4d7ec453..9d796a16 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -80,22 +80,22 @@ struct NodeList: View { ) } - if let connectedNode { - ExchangePositionsButton( - bleManager: bleManager, - node: node - ) - TraceRouteButton( - bleManager: bleManager, - node: node - ) - DeleteNodeButton( - bleManager: bleManager, - context: context, - connectedNode: connectedNode, - node: node - ) - } +// if let connectedNode { +// ExchangePositionsButton( +// bleManager: bleManager, +// node: node +// ) +// TraceRouteButton( +// bleManager: bleManager, +// node: node +// ) +// DeleteNodeButton( +// bleManager: bleManager, +// context: context, +// connectedNode: connectedNode, +// node: node +// ) +// } } var body: some View { From f5e48a577600724813c39802a9c8c5933f12fdae Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Jul 2024 21:27:42 -0700 Subject: [PATCH 009/333] Fix setting display availability --- Meshtastic/Views/Settings/Settings.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 414daf0f..b141f997 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -444,7 +444,7 @@ struct Settings: View { case .power: PowerConfig(node: node) case .ambientLighting: - if #available(iOS 17.0, *) { + if #available(iOS 17.0, macOS 14.0, *) { AmbientLightingConfig(node: node) } case .cannedMessages: @@ -470,7 +470,7 @@ struct Settings: View { case .meshLog: MeshLog() case .debugLogs: - if #available(iOS 17.4, *) { + if #available(iOS 17.0, macOS 14.0, *) { AppLog() } case .appFiles: From 9bd0021facffa5e7167a2676c900be0d33e579e0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Jul 2024 21:37:18 -0700 Subject: [PATCH 010/333] Content unavailable updates --- Localizable.xcstrings | 6 +++--- Meshtastic/Views/Settings/AppLog.swift | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a2953404..f5642a53 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -10849,6 +10849,9 @@ }, "Line Series" : { + }, + "Loading Logs. . ." : { + }, "Location: %@" : { @@ -15301,9 +15304,6 @@ }, "No Environment Metrics" : { - }, - "No Logs Available" : { - }, "No Positions" : { diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index 0a2aaad0..fd3db810 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -58,7 +58,6 @@ struct AppLog: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - } .controlSize(.regular) .padding(5) @@ -69,7 +68,7 @@ struct AppLog: View { .disabled(selection != nil) .overlay { if logs.isEmpty { - ContentUnavailableView("No Logs Available", systemImage: "scroll") + ContentUnavailableView("Loading Logs. . .", systemImage: "scroll") } } .refreshable { @@ -120,7 +119,7 @@ struct AppLog: View { .disabled(selection != nil) .overlay { if logs.isEmpty { - ContentUnavailableView("No Logs Available", systemImage: "scroll") + ContentUnavailableView("Loading Logs. . .", systemImage: "scroll") } } .refreshable { From 73ffb056632c5e41d65cc2593720fb351f3d701d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 31 Jul 2024 14:47:31 -0700 Subject: [PATCH 011/333] Hide just broken delete button --- Meshtastic/Views/Nodes/NodeList.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 9d796a16..ac9aa65e 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -80,22 +80,22 @@ struct NodeList: View { ) } -// if let connectedNode { -// ExchangePositionsButton( -// bleManager: bleManager, -// node: node -// ) -// TraceRouteButton( -// bleManager: bleManager, -// node: node -// ) + if let connectedNode { + ExchangePositionsButton( + bleManager: bleManager, + node: node + ) + TraceRouteButton( + bleManager: bleManager, + node: node + ) // DeleteNodeButton( // bleManager: bleManager, // context: context, // connectedNode: connectedNode, // node: node // ) -// } + } } var body: some View { From 3032a4d4dee6d00418613477976bd4b0729384cc Mon Sep 17 00:00:00 2001 From: ChDel Date: Tue, 23 Jul 2024 19:10:00 -0700 Subject: [PATCH 012/333] Changes to support iOS 18 Beta --- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- .../CoreData/ChannelEntityExtension.swift | 5 ++++- .../CoreData/MessageEntityExtension.swift | 8 ++++++++ .../CoreData/MyInfoEntityExtension.swift | 6 +++++- .../Extensions/CoreData/UserEntityExtension.swift | 14 +++++++++++--- Meshtastic/Views/Messages/TapbackResponses.swift | 2 +- 6 files changed, 32 insertions(+), 9 deletions(-) diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 10db40a7..e358787d 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "74b3ad6215f078d89f4436b6ce0e318f145842efa3453bbe055ab76057de7d6b", + "originHash" : "af29d93455cb8f728684674f544d815b5becb17e049287cc1df8079a4855d0fc", "pins" : [ { "identity" : "cocoamqtt", @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", - "version" : "1.26.0" + "revision" : "d57a5aecf24a25b32ec4a74be2f5d0a995a47c4b", + "version" : "1.27.0" } } ], diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 62f7eff0..2ff2746c 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -11,8 +11,11 @@ import MeshtasticProtobufs extension ChannelEntity { var allPrivateMessages: [MessageEntity] { + let context = PersistenceController.shared.container.viewContext + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) - self.value(forKey: "allPrivateMessages") as? [MessageEntity] ?? [MessageEntity]() + return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } var unreadMessages: Int { diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 81e106b8..a7ba0e78 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -21,4 +21,12 @@ extension MessageEntity { var canRetry: Bool { return ackError == 9 || ackError == 5 || ackError == 3 } + + var tapbacks: [MessageEntity] { + let context = PersistenceController.shared.container.viewContext + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "replyID == %lld AND isEmoji == true", self.messageId) + + return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + } } diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index 80ba19c8..d2172400 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -10,7 +10,11 @@ import Foundation extension MyInfoEntity { var messageList: [MessageEntity] { - self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]() + let context = PersistenceController.shared.container.viewContext + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "toUser == nil") + + return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } var unreadMessages: Int { diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index a702fb6d..c91ce34b 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -12,18 +12,26 @@ import MeshtasticProtobufs extension UserEntity { var messageList: [MessageEntity] { - self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]() + let context = PersistenceController.shared.container.viewContext + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "((toUser == %@) OR (fromUser == %@)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10", self, self) + + return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } var sensorMessageList: [MessageEntity] { - self.value(forKey: "detectionSensorMessages") as? [MessageEntity] ?? [MessageEntity]() + let context = PersistenceController.shared.container.viewContext + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "(fromUser == %@) AND portNum = 10", self) + + return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } var unreadMessages: Int { let unreadMessages = messageList.filter { ($0 as AnyObject).read == false } return unreadMessages.count } - + var hardwareImage: String? { guard let hwModel else { return nil } switch hwModel { diff --git a/Meshtastic/Views/Messages/TapbackResponses.swift b/Meshtastic/Views/Messages/TapbackResponses.swift index fb61b44b..77310184 100644 --- a/Meshtastic/Views/Messages/TapbackResponses.swift +++ b/Meshtastic/Views/Messages/TapbackResponses.swift @@ -9,7 +9,7 @@ struct TapbackResponses: View { @ViewBuilder var body: some View { - let tapbacks = message.value(forKey: "tapbacks") as? [MessageEntity] ?? [] + let tapbacks = message.tapbacks if !tapbacks.isEmpty { VStack(alignment: .trailing) { HStack { From d49b83685fa9a0acd8e76db0720770765032ea62 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 31 Jul 2024 15:21:59 -0700 Subject: [PATCH 013/333] Remove fetched properties --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 449 ++++++++++++++++++ 3 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 49bf08ef..b8ff83a2 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -298,6 +298,7 @@ DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 35.xcdatamodel"; sourceTree = ""; }; DD268D8D2BCC90E2008073AE /* RouteEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteEnums.swift; sourceTree = ""; }; DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; + DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 41.xcdatamodel"; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 36.xcdatamodel"; sourceTree = ""; }; @@ -1815,6 +1816,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */, DD3D17DC2C3D7B1400561584 /* MeshtasticDataModelV 39.xcdatamodel */, DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */, @@ -1856,7 +1858,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */; + currentVersion = DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 9ae100e3..9deb5562 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 40.xcdatamodel + MeshtasticDataModelV 41.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents new file mode 100644 index 00000000..36f6fabd --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 33978592d3460acef05e2e4e9c95d3577db7d42f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 31 Jul 2024 20:56:09 -0700 Subject: [PATCH 014/333] Remove lightly used receivedTimestamp --- Meshtastic/Extensions/CoreData/MessageEntityExtension.swift | 2 +- Meshtastic/Helpers/MeshPackets.swift | 1 - .../MeshtasticDataModelV 41.xcdatamodel/contents | 1 - Meshtastic/Persistence/QueryCoreData.swift | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index a7ba0e78..db3a0899 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -14,7 +14,7 @@ import SwiftUI extension MessageEntity { var timestamp: Date { - let time = messageTimestamp <= 0 ? receivedTimestamp : messageTimestamp + let time = messageTimestamp return Date(timeIntervalSince1970: TimeInterval(time)) } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 0d6f5917..bb3687b5 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -795,7 +795,6 @@ func textMessageAppPacket( let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) - newMessage.receivedTimestamp = Int32(Date().timeIntervalSince1970) newMessage.receivedACK = false newMessage.snr = packet.rxSnr newMessage.rssi = packet.rxRssi diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents index 36f6fabd..55eccab7 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents @@ -159,7 +159,6 @@ - diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index f023eb9b..55889764 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -29,7 +29,7 @@ public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectC let fetchMessagesRequest = MessageEntity.fetchRequest() let timeRange = Calendar.current.date(byAdding: .minute, value: time, to: Date()) let milleseconds = Int32(timeRange?.timeIntervalSince1970 ?? 0) - fetchMessagesRequest.predicate = NSPredicate(format: "receivedTimestamp >= %d", milleseconds) + fetchMessagesRequest.predicate = NSPredicate(format: "messageTimestamp >= %d", milleseconds) do { let fetchedMessages = try context.fetch(fetchMessagesRequest) From cc4d77a91540976c008f4d19e5b51b26ba6f276e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 31 Jul 2024 21:03:40 -0700 Subject: [PATCH 015/333] Update Protos --- .../xcschemes/WidgetsExtension.xcscheme | 1 + .../Sources/meshtastic/mesh.pb.swift | 16 + .../Sources/meshtastic/module_config.pb.swift | 7 + .../Sources/meshtastic/telemetry.pb.swift | 335 +++++++++++++----- protobufs | 2 +- 5 files changed, 265 insertions(+), 96 deletions(-) diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme index 880339bc..decd8381 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme @@ -89,6 +89,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 57e8bde4..6a2906fe 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -321,6 +321,14 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// specifically adapted for the Meshtatic project case heltecMeshNodeT114 // = 69 + /// + /// Sensecap Indicator from Seeed Studio. ESP32-S3 device with TFT and RP2040 coprocessor + case sensecapIndicator // = 70 + + /// + /// Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors. + case trackerT1000E // = 71 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -403,6 +411,8 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 67: self = .heltecVisionMasterE213 case 68: self = .heltecVisionMasterE290 case 69: self = .heltecMeshNodeT114 + case 70: self = .sensecapIndicator + case 71: self = .trackerT1000E case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -479,6 +489,8 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .heltecVisionMasterE213: return 67 case .heltecVisionMasterE290: return 68 case .heltecMeshNodeT114: return 69 + case .sensecapIndicator: return 70 + case .trackerT1000E: return 71 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -560,6 +572,8 @@ extension HardwareModel: CaseIterable { .heltecVisionMasterE213, .heltecVisionMasterE290, .heltecMeshNodeT114, + .sensecapIndicator, + .trackerT1000E, .privateHw, ] } @@ -3027,6 +3041,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 67: .same(proto: "HELTEC_VISION_MASTER_E213"), 68: .same(proto: "HELTEC_VISION_MASTER_E290"), 69: .same(proto: "HELTEC_MESH_NODE_T114"), + 70: .same(proto: "SENSECAP_INDICATOR"), + 71: .same(proto: "TRACKER_T1000_E"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 44fd0902..c68ffd83 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -758,6 +758,9 @@ public struct ModuleConfig { /// NMEA messages specifically tailored for CalTopo case caltopo // = 5 + + /// Ecowitt WS85 weather station + case ws85 // = 6 case UNRECOGNIZED(Int) public init() { @@ -772,6 +775,7 @@ public struct ModuleConfig { case 3: self = .textmsg case 4: self = .nmea case 5: self = .caltopo + case 6: self = .ws85 default: self = .UNRECOGNIZED(rawValue) } } @@ -784,6 +788,7 @@ public struct ModuleConfig { case .textmsg: return 3 case .nmea: return 4 case .caltopo: return 5 + case .ws85: return 6 case .UNRECOGNIZED(let i): return i } } @@ -1208,6 +1213,7 @@ extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { .textmsg, .nmea, .caltopo, + .ws85, ] } @@ -2081,6 +2087,7 @@ extension ModuleConfig.SerialConfig.Serial_Mode: SwiftProtobuf._ProtoNameProvidi 3: .same(proto: "TEXTMSG"), 4: .same(proto: "NMEA"), 5: .same(proto: "CALTOPO"), + 6: .same(proto: "WS85"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index c0c17cf4..ec627e3d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -277,69 +277,130 @@ public struct EnvironmentMetrics { /// /// Temperature measured - public var temperature: Float = 0 + public var temperature: Float { + get {return _storage._temperature} + set {_uniqueStorage()._temperature = newValue} + } /// /// Relative humidity percent measured - public var relativeHumidity: Float = 0 + public var relativeHumidity: Float { + get {return _storage._relativeHumidity} + set {_uniqueStorage()._relativeHumidity = newValue} + } /// /// Barometric pressure in hPA measured - public var barometricPressure: Float = 0 + public var barometricPressure: Float { + get {return _storage._barometricPressure} + set {_uniqueStorage()._barometricPressure = newValue} + } /// /// Gas resistance in MOhm measured - public var gasResistance: Float = 0 + public var gasResistance: Float { + get {return _storage._gasResistance} + set {_uniqueStorage()._gasResistance = newValue} + } /// /// Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) - public var voltage: Float = 0 + public var voltage: Float { + get {return _storage._voltage} + set {_uniqueStorage()._voltage = newValue} + } /// /// Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) - public var current: Float = 0 + public var current: Float { + get {return _storage._current} + set {_uniqueStorage()._current = newValue} + } /// /// relative scale IAQ value as measured by Bosch BME680 . value 0-500. /// Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. - public var iaq: UInt32 = 0 + public var iaq: UInt32 { + get {return _storage._iaq} + set {_uniqueStorage()._iaq = newValue} + } /// /// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. - public var distance: Float = 0 + public var distance: Float { + get {return _storage._distance} + set {_uniqueStorage()._distance = newValue} + } /// /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. - public var lux: Float = 0 + public var lux: Float { + get {return _storage._lux} + set {_uniqueStorage()._lux = newValue} + } /// /// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. - public var whiteLux: Float = 0 + public var whiteLux: Float { + get {return _storage._whiteLux} + set {_uniqueStorage()._whiteLux = newValue} + } /// /// Infrared lux - public var irLux: Float = 0 + public var irLux: Float { + get {return _storage._irLux} + set {_uniqueStorage()._irLux = newValue} + } /// /// Ultraviolet lux - public var uvLux: Float = 0 + public var uvLux: Float { + get {return _storage._uvLux} + set {_uniqueStorage()._uvLux = newValue} + } /// /// Wind direction in degrees /// 0 degrees = North, 90 = East, etc... - public var windDirection: UInt32 = 0 + public var windDirection: UInt32 { + get {return _storage._windDirection} + set {_uniqueStorage()._windDirection = newValue} + } /// /// Wind speed in m/s - public var windSpeed: Float = 0 + public var windSpeed: Float { + get {return _storage._windSpeed} + set {_uniqueStorage()._windSpeed = newValue} + } /// /// Weight in KG - public var weight: Float = 0 + public var weight: Float { + get {return _storage._weight} + set {_uniqueStorage()._weight = newValue} + } + + /// + /// Wind gust in m/s + public var windGust: Float { + get {return _storage._windGust} + set {_uniqueStorage()._windGust = newValue} + } + + /// + /// Wind lull in m/s + public var windLull: Float { + get {return _storage._windLull} + set {_uniqueStorage()._windLull = newValue} + } public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance } /// @@ -678,99 +739,183 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 13: .standard(proto: "wind_direction"), 14: .standard(proto: "wind_speed"), 15: .same(proto: "weight"), + 16: .standard(proto: "wind_gust"), + 17: .standard(proto: "wind_lull"), ] + fileprivate class _StorageClass { + var _temperature: Float = 0 + var _relativeHumidity: Float = 0 + var _barometricPressure: Float = 0 + var _gasResistance: Float = 0 + var _voltage: Float = 0 + var _current: Float = 0 + var _iaq: UInt32 = 0 + var _distance: Float = 0 + var _lux: Float = 0 + var _whiteLux: Float = 0 + var _irLux: Float = 0 + var _uvLux: Float = 0 + var _windDirection: UInt32 = 0 + var _windSpeed: Float = 0 + var _weight: Float = 0 + var _windGust: Float = 0 + var _windLull: Float = 0 + + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif + + private init() {} + + init(copying source: _StorageClass) { + _temperature = source._temperature + _relativeHumidity = source._relativeHumidity + _barometricPressure = source._barometricPressure + _gasResistance = source._gasResistance + _voltage = source._voltage + _current = source._current + _iaq = source._iaq + _distance = source._distance + _lux = source._lux + _whiteLux = source._whiteLux + _irLux = source._irLux + _uvLux = source._uvLux + _windDirection = source._windDirection + _windSpeed = source._windSpeed + _weight = source._weight + _windGust = source._windGust + _windLull = source._windLull + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + 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.decodeSingularFloatField(value: &self.temperature) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.relativeHumidity) }() - case 3: try { try decoder.decodeSingularFloatField(value: &self.barometricPressure) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.gasResistance) }() - case 5: try { try decoder.decodeSingularFloatField(value: &self.voltage) }() - case 6: try { try decoder.decodeSingularFloatField(value: &self.current) }() - case 7: try { try decoder.decodeSingularUInt32Field(value: &self.iaq) }() - case 8: try { try decoder.decodeSingularFloatField(value: &self.distance) }() - case 9: try { try decoder.decodeSingularFloatField(value: &self.lux) }() - case 10: try { try decoder.decodeSingularFloatField(value: &self.whiteLux) }() - case 11: try { try decoder.decodeSingularFloatField(value: &self.irLux) }() - case 12: try { try decoder.decodeSingularFloatField(value: &self.uvLux) }() - case 13: try { try decoder.decodeSingularUInt32Field(value: &self.windDirection) }() - case 14: try { try decoder.decodeSingularFloatField(value: &self.windSpeed) }() - case 15: try { try decoder.decodeSingularFloatField(value: &self.weight) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + 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.decodeSingularFloatField(value: &_storage._temperature) }() + case 2: try { try decoder.decodeSingularFloatField(value: &_storage._relativeHumidity) }() + case 3: try { try decoder.decodeSingularFloatField(value: &_storage._barometricPressure) }() + case 4: try { try decoder.decodeSingularFloatField(value: &_storage._gasResistance) }() + case 5: try { try decoder.decodeSingularFloatField(value: &_storage._voltage) }() + case 6: try { try decoder.decodeSingularFloatField(value: &_storage._current) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._iaq) }() + case 8: try { try decoder.decodeSingularFloatField(value: &_storage._distance) }() + case 9: try { try decoder.decodeSingularFloatField(value: &_storage._lux) }() + case 10: try { try decoder.decodeSingularFloatField(value: &_storage._whiteLux) }() + case 11: try { try decoder.decodeSingularFloatField(value: &_storage._irLux) }() + case 12: try { try decoder.decodeSingularFloatField(value: &_storage._uvLux) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &_storage._windDirection) }() + case 14: try { try decoder.decodeSingularFloatField(value: &_storage._windSpeed) }() + case 15: try { try decoder.decodeSingularFloatField(value: &_storage._weight) }() + case 16: try { try decoder.decodeSingularFloatField(value: &_storage._windGust) }() + case 17: try { try decoder.decodeSingularFloatField(value: &_storage._windLull) }() + default: break + } } } } public func traverse(visitor: inout V) throws { - if self.temperature != 0 { - try visitor.visitSingularFloatField(value: self.temperature, fieldNumber: 1) - } - if self.relativeHumidity != 0 { - try visitor.visitSingularFloatField(value: self.relativeHumidity, fieldNumber: 2) - } - if self.barometricPressure != 0 { - try visitor.visitSingularFloatField(value: self.barometricPressure, fieldNumber: 3) - } - if self.gasResistance != 0 { - try visitor.visitSingularFloatField(value: self.gasResistance, fieldNumber: 4) - } - if self.voltage != 0 { - try visitor.visitSingularFloatField(value: self.voltage, fieldNumber: 5) - } - if self.current != 0 { - try visitor.visitSingularFloatField(value: self.current, fieldNumber: 6) - } - if self.iaq != 0 { - try visitor.visitSingularUInt32Field(value: self.iaq, fieldNumber: 7) - } - if self.distance != 0 { - try visitor.visitSingularFloatField(value: self.distance, fieldNumber: 8) - } - if self.lux != 0 { - try visitor.visitSingularFloatField(value: self.lux, fieldNumber: 9) - } - if self.whiteLux != 0 { - try visitor.visitSingularFloatField(value: self.whiteLux, fieldNumber: 10) - } - if self.irLux != 0 { - try visitor.visitSingularFloatField(value: self.irLux, fieldNumber: 11) - } - if self.uvLux != 0 { - try visitor.visitSingularFloatField(value: self.uvLux, fieldNumber: 12) - } - if self.windDirection != 0 { - try visitor.visitSingularUInt32Field(value: self.windDirection, fieldNumber: 13) - } - if self.windSpeed != 0 { - try visitor.visitSingularFloatField(value: self.windSpeed, fieldNumber: 14) - } - if self.weight != 0 { - try visitor.visitSingularFloatField(value: self.weight, fieldNumber: 15) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + if _storage._temperature != 0 { + try visitor.visitSingularFloatField(value: _storage._temperature, fieldNumber: 1) + } + if _storage._relativeHumidity != 0 { + try visitor.visitSingularFloatField(value: _storage._relativeHumidity, fieldNumber: 2) + } + if _storage._barometricPressure != 0 { + try visitor.visitSingularFloatField(value: _storage._barometricPressure, fieldNumber: 3) + } + if _storage._gasResistance != 0 { + try visitor.visitSingularFloatField(value: _storage._gasResistance, fieldNumber: 4) + } + if _storage._voltage != 0 { + try visitor.visitSingularFloatField(value: _storage._voltage, fieldNumber: 5) + } + if _storage._current != 0 { + try visitor.visitSingularFloatField(value: _storage._current, fieldNumber: 6) + } + if _storage._iaq != 0 { + try visitor.visitSingularUInt32Field(value: _storage._iaq, fieldNumber: 7) + } + if _storage._distance != 0 { + try visitor.visitSingularFloatField(value: _storage._distance, fieldNumber: 8) + } + if _storage._lux != 0 { + try visitor.visitSingularFloatField(value: _storage._lux, fieldNumber: 9) + } + if _storage._whiteLux != 0 { + try visitor.visitSingularFloatField(value: _storage._whiteLux, fieldNumber: 10) + } + if _storage._irLux != 0 { + try visitor.visitSingularFloatField(value: _storage._irLux, fieldNumber: 11) + } + if _storage._uvLux != 0 { + try visitor.visitSingularFloatField(value: _storage._uvLux, fieldNumber: 12) + } + if _storage._windDirection != 0 { + try visitor.visitSingularUInt32Field(value: _storage._windDirection, fieldNumber: 13) + } + if _storage._windSpeed != 0 { + try visitor.visitSingularFloatField(value: _storage._windSpeed, fieldNumber: 14) + } + if _storage._weight != 0 { + try visitor.visitSingularFloatField(value: _storage._weight, fieldNumber: 15) + } + if _storage._windGust != 0 { + try visitor.visitSingularFloatField(value: _storage._windGust, fieldNumber: 16) + } + if _storage._windLull != 0 { + try visitor.visitSingularFloatField(value: _storage._windLull, fieldNumber: 17) + } } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: EnvironmentMetrics, rhs: EnvironmentMetrics) -> Bool { - if lhs.temperature != rhs.temperature {return false} - if lhs.relativeHumidity != rhs.relativeHumidity {return false} - if lhs.barometricPressure != rhs.barometricPressure {return false} - if lhs.gasResistance != rhs.gasResistance {return false} - if lhs.voltage != rhs.voltage {return false} - if lhs.current != rhs.current {return false} - if lhs.iaq != rhs.iaq {return false} - if lhs.distance != rhs.distance {return false} - if lhs.lux != rhs.lux {return false} - if lhs.whiteLux != rhs.whiteLux {return false} - if lhs.irLux != rhs.irLux {return false} - if lhs.uvLux != rhs.uvLux {return false} - if lhs.windDirection != rhs.windDirection {return false} - if lhs.windSpeed != rhs.windSpeed {return false} - if lhs.weight != rhs.weight {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._temperature != rhs_storage._temperature {return false} + if _storage._relativeHumidity != rhs_storage._relativeHumidity {return false} + if _storage._barometricPressure != rhs_storage._barometricPressure {return false} + if _storage._gasResistance != rhs_storage._gasResistance {return false} + if _storage._voltage != rhs_storage._voltage {return false} + if _storage._current != rhs_storage._current {return false} + if _storage._iaq != rhs_storage._iaq {return false} + if _storage._distance != rhs_storage._distance {return false} + if _storage._lux != rhs_storage._lux {return false} + if _storage._whiteLux != rhs_storage._whiteLux {return false} + if _storage._irLux != rhs_storage._irLux {return false} + if _storage._uvLux != rhs_storage._uvLux {return false} + if _storage._windDirection != rhs_storage._windDirection {return false} + if _storage._windSpeed != rhs_storage._windSpeed {return false} + if _storage._weight != rhs_storage._weight {return false} + if _storage._windGust != rhs_storage._windGust {return false} + if _storage._windLull != rhs_storage._windLull {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index d191975e..97674883 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit d191975ebc572527c6d9eec48d5b0a1e3331999f +Subproject commit 976748839fafcf0049bb364fe2c7226a194d18a9 From f7e48602f00de601cdd04ddee034e10dc9a1c27d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 07:11:41 -0700 Subject: [PATCH 016/333] Bump versions --- Meshtastic.xcodeproj/project.pbxproj | 2 +- Meshtastic/Views/Settings/Firmware.swift | 2 +- protobufs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b8ff83a2..17b498b9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1634,7 +1634,7 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 87f2bb88..e591761f 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -13,7 +13,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.3.15" + @State var minimumVersion = "2.4.0" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? diff --git a/protobufs b/protobufs index 10494bf3..97674883 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 10494bf328ac051fc4add9ddeb677eebf337b531 +Subproject commit 976748839fafcf0049bb364fe2c7226a194d18a9 From c9749eaffeabeaf0e5da5b204d177557068bb406 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 07:49:38 -0700 Subject: [PATCH 017/333] Fix admin channel for settings other than lora config --- Meshtastic/Views/Settings/Settings.swift | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index b141f997..faae073e 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -430,43 +430,43 @@ struct Settings: View { case .shareQRCode: ShareChannels(node: node) case .user: - UserConfig(node: node) + UserConfig(node: nodes.first(where: { $0.num == selectedNode })) case .bluetooth: - BluetoothConfig(node: node) + BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) case .device: - DeviceConfig(node: node) + DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) case .display: - DisplayConfig(node: node) + DisplayConfig(node: nodes.first(where: { $0.num == selectedNode })) case .network: - NetworkConfig(node: node) + NetworkConfig(node: nodes.first(where: { $0.num == selectedNode })) case .position: - PositionConfig(node: node) + PositionConfig(node: nodes.first(where: { $0.num == selectedNode })) case .power: - PowerConfig(node: node) + PowerConfig(node: nodes.first(where: { $0.num == selectedNode })) case .ambientLighting: if #available(iOS 17.0, macOS 14.0, *) { AmbientLightingConfig(node: node) } case .cannedMessages: - CannedMessagesConfig(node: node) + CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) case .detectionSensor: - DetectionSensorConfig(node: node) + DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode })) case .externalNotification: - ExternalNotificationConfig(node: node) + ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode })) case .mqtt: - MQTTConfig(node: node) + MQTTConfig(node: nodes.first(where: { $0.num == selectedNode })) case .rangeTest: - RangeTestConfig(node: node) + RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode })) case .paxCounter: - PaxCounterConfig(node: node) + PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) case .ringtone: - RtttlConfig(node: node) + RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) case .serial: - SerialConfig(node: node) + SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) case .storeAndForward: - StoreForwardConfig(node: node) + StoreForwardConfig(node: nodes.first(where: { $0.num == selectedNode })) case .telemetry: - TelemetryConfig(node: node) + TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) case .meshLog: MeshLog() case .debugLogs: From 1b77513b2f95619b1c289394b1015c5093182b3f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 08:00:27 -0700 Subject: [PATCH 018/333] Remove broken MB tiles funcitionality --- Localizable.xcstrings | 6 --- Meshtastic/Extensions/UserDefaults.swift | 4 -- Meshtastic/MeshtasticApp.swift | 42 --------------- .../MapKitMap/Custom/MapViewSwiftUI.swift | 42 +++------------ Meshtastic/Views/Nodes/NodeMap.swift | 54 +++++++------------ 5 files changed, 26 insertions(+), 122 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b0f415a3..967567c0 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6814,9 +6814,6 @@ }, "Empty" : { - }, - "Enable MB Tiles" : { - }, "Enable Notifications" : { @@ -20901,9 +20898,6 @@ }, "The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long." : { - }, - "The latest MBTiles file shared with meshtastic will be loaded into the map." : { - }, "The maximum interval that can elapse without a node broadcasting a position" : { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 11049bfb..0fbd680b 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -57,7 +57,6 @@ extension UserDefaults { case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps - case enableOfflineMapsMBTiles case mapTileServer case enableOverlayServer case mapOverlayServer @@ -121,9 +120,6 @@ extension UserDefaults { @UserDefault(.enableOfflineMaps, defaultValue: false) static var enableOfflineMaps: Bool - @UserDefault(.enableOfflineMapsMBTiles, defaultValue: false) - static var enableOfflineMapsMBTiles: Bool - @UserDefault(.mapTileServer, defaultValue: .openStreetMap) static var mapTileServer: MapTileServer diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index c1bcf91a..ff691440 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -110,48 +110,6 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") } else if url.absoluteString.lowercased().contains("meshtastic:///") { appState.router.route(url: url) - } else { - saveChannels = false - Logger.mesh.debug("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")") - } - - /// Only do the map tiles stuff if it is enabled - if UserDefaults.enableOfflineMapsMBTiles { - /// we are expecting a .mbtiles map file that contains raster data - /// save it to the documents directory, and name it offline_map.mbtiles - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false) - - if !self.saveChannels { - - // tell the system we want the file please - guard url.startAccessingSecurityScopedResource() else { - return - } - - // do we need to delete an old one? - if fileManager.fileExists(atPath: destination.path) { - Logger.mesh.info("Found an old map file. Deleting it") - try? fileManager.removeItem(atPath: destination.path) - } - - do { - try fileManager.copyItem(at: url, to: destination) - } catch { - Logger.mesh.error("Copy MB Tile file failed. Error: \(error.localizedDescription)") - } - - if fileManager.fileExists(atPath: destination.path) { - Logger.mesh.info("Saved the map file") - - // need to tell the map view that it needs to update and try loading the new overlay - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile") - - } else { - Logger.mesh.error("Didn't save the map file") - } - } } }) .task { diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index 5c7291a5..bf196f6d 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -105,13 +105,12 @@ struct MapViewSwiftUI: UIViewRepresentable { switch selectedMapLayer { case .offline: mapView.mapType = .standard - if !UserDefaults.enableOfflineMapsMBTiles { - let overlay = TileOverlay() - overlay.canReplaceMapContent = false - overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex - overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex - mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) - } + let overlay = TileOverlay() + overlay.canReplaceMapContent = false + overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex + overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex + mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) + case .satellite: mapView.mapType = .satellite case .hybrid: @@ -133,32 +132,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - private func setMbtilesOverlay(mapView: MKMapView) { - // MBTiles Offline - if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles { - if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { - mapView.removeOverlays(mapView.overlays) - if self.customMapOverlay != nil { - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path - if fileManager.fileExists(atPath: tilePath) { - Logger.services.info("Loading local map file") - if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) { - overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent - mapView.addOverlay(overlay) - } - } else { - Logger.services.info("Couldn't find a local map file to load") - } - } - DispatchQueue.main.async { - self.presentCustomMapOverlayHash = self.customMapOverlay - self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile - } - } - } - } + func makeUIView(context: Context) -> MKMapView { currentMapLayer = nil mapView.delegate = context.coordinator @@ -166,8 +140,6 @@ struct MapViewSwiftUI: UIViewRepresentable { return mapView } func updateUIView(_ mapView: MKMapView, context: Context) { - // Set MBTiles overlay layer - setMbtilesOverlay(mapView: mapView) // Set selected map base layer setMapBaseLayer(mapView: mapView) // Set map tile server and weather overlay layers diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 5630fa8f..53b0aae3 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -16,14 +16,12 @@ struct NodeMap: View { @ObservedObject var router: Router - @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer - @State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels @@ -172,46 +170,32 @@ struct NodeMap: View { } if enableOfflineMaps { VStack(alignment: .leading) { - if !enableOfflineMapsMBTiles { - Picker(selection: $selectedTileServer, - label: Text("Tile Server")) { - ForEach(MapTileServer.allCases, id: \.self) { tsl in - Text(tsl.description) - } - } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedTileServer)) { newSelectedTileServer in - UserDefaults.mapTileServer = newSelectedTileServer - } - Text("Attribution:") - .fontWeight(.semibold) - .font(.footnote) - Text(LocalizedStringKey(selectedTileServer.attribution)) - .font(.footnote) - .foregroundColor(.gray) - .padding(0) - Divider() - Toggle(isOn: $mapTilesAboveLabels) { - Text("Tiles above Labels") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.mapTilesAboveLabels.toggle() - UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels + Picker(selection: $selectedTileServer, + label: Text("Tile Server")) { + ForEach(MapTileServer.allCases, id: \.self) { tsl in + Text(tsl.description) } } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedTileServer)) { newSelectedTileServer in + UserDefaults.mapTileServer = newSelectedTileServer + } + Text("Attribution:") + .fontWeight(.semibold) + .font(.footnote) + Text(LocalizedStringKey(selectedTileServer.attribution)) + .font(.footnote) + .foregroundColor(.gray) + .padding(0) Divider() - Toggle(isOn: $enableOfflineMapsMBTiles) { - Text("Enable MB Tiles") + Toggle(isOn: $mapTilesAboveLabels) { + Text("Tiles above Labels") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .onTapGesture { - self.enableOfflineMapsMBTiles.toggle() - UserDefaults.enableOfflineMapsMBTiles = self.enableOfflineMapsMBTiles + self.mapTilesAboveLabels.toggle() + UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels } - Text("The latest MBTiles file shared with meshtastic will be loaded into the map.") - .font(.footnote) - .foregroundColor(.gray) } } } From 3745929528514de053ae206a668cb992811d3fed Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 18:44:49 -0700 Subject: [PATCH 019/333] Bump Version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 17b498b9..557c62b6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1604,7 +1604,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.4.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1639,7 +1639,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.4.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1671,7 +1671,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.4.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1704,7 +1704,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.4.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 6b9cc60c2fa01c020dd07a87ee0a0118573895a9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 18:53:44 -0700 Subject: [PATCH 020/333] Update Readme --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index a72c6ebb..f10d5531 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Meshtastic Apple Clients - - Meshtastic App Store Launch Image - - ## Overview SwiftUI client applications for iOS, iPadOS and macOS. From 48a57eaa13cff0c3b9cdd6178854831b436efaf0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 18:57:24 -0700 Subject: [PATCH 021/333] Delete orphaned file --- Meshtastic.xcodeproj/project.pbxproj | 4 - .../MapKitMap/Custom/LocalMBTileOverlay.swift | 154 ------------------ 2 files changed, 158 deletions(-) delete mode 100644 Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 557c62b6..dca5ef1c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -32,7 +32,6 @@ 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; - C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; @@ -261,7 +260,6 @@ 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; - C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -558,7 +556,6 @@ C9A7BC0E27759A6800760B50 /* Custom */ = { isa = PBXGroup; children = ( - C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, DD964FC32974767D007C176F /* MapViewFitExtension.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, DDDB443529F6287000EE2349 /* MapButtons.swift */, @@ -1361,7 +1358,6 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, - C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, diff --git a/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift b/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift deleted file mode 100644 index f16d63e3..00000000 --- a/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// LocalMBTileOverlay.swift -// MeshtasticApple -// -// Copyright(c) Joshua Pirihi 16/01/22. -// - -import UIKit -import MapKit -import SQLite -import OSLog - -extension MKMapRect { - init(coordinates: [CLLocationCoordinate2D]) { - self = MKMapRect() - var coordinates = coordinates - if !coordinates.isEmpty { - let first = coordinates.removeFirst() - var top = first.latitude - var bottom = first.latitude - var left = first.longitude - var right = first.longitude - coordinates.forEach { coordinate in - top = max(top, coordinate.latitude) - bottom = min(bottom, coordinate.latitude) - left = min(left, coordinate.longitude) - right = max(right, coordinate.longitude) - } - let topLeft = MKMapPoint(CLLocationCoordinate2D(latitude: top, longitude: left)) - let bottomRight = MKMapPoint(CLLocationCoordinate2D(latitude: bottom, longitude: right)) - self = MKMapRect(x: topLeft.x, y: topLeft.y, - width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y) - } - } -} - -enum MapTileError: Error { - case invalidFormat - case other -} - -class LocalMBTileOverlay: MKTileOverlay { - - var path: String! - var mb: Connection! - private var _boundingMapRect: MKMapRect! - override var boundingMapRect: MKMapRect { - return _boundingMapRect - } - - init?(mbTilePath path: String) { - - super.init(urlTemplate: nil) - self.path = path - do { - self.mb = try Connection(self.path, readonly: true) - let metadata = Table("metadata") - - let name = Expression(value: "name") - let value = Expression(value: "value") - - // make sure it's raster - let formatQuery = try mb.pluck(metadata.select(value).filter(name == "format")) - if formatQuery?[value] == nil || (formatQuery![value] != "jpeg" && formatQuery![value] != "jpg" && formatQuery![value] != "png") { - throw MapTileError.invalidFormat - } - - let minZQuery = try mb.pluck(metadata.select(value).filter(name == "minzoom")) - self.minimumZ = Int(minZQuery![value])! - - let maxZQuery = try mb.pluck(metadata.select(value).filter(name == "maxzoom")) - self.maximumZ = Int(maxZQuery![value])! - - self.isGeometryFlipped = true - - let boundingBoxString = try mb.pluck(metadata.select(value).filter(name == "bounds")) - let boundCoords = boundingBoxString![value].split(separator: ",") - let coords = [ - CLLocationCoordinate2D(latitude: Double(boundCoords[1]) ?? 0, - longitude: Double(boundCoords[0]) ?? 0), - CLLocationCoordinate2D(latitude: Double(boundCoords[3]) ?? 0, - longitude: Double(boundCoords[2]) ?? 0) - ] - self._boundingMapRect = MKMapRect(coordinates: coords) - - } catch { - Logger.services.error("Map tile error: \(error)") - return nil - } - } - -// override func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void) { -// -// let tileX = Int64(path.x) -// let tileY = Int64(path.y) -// let tileZ = Int64(path.z) -// let tileData = Expression("tile_data") -// let zoomLevel = Expression("zoom_level") -// let tileColumn = Expression("tile_column") -// let tileRow = Expression("tile_row") -// -// if let dataQuery = try? self.mb.pluck(Table("tiles").select(tileData).filter(zoomLevel == tileZ).filter(tileColumn == tileX).filter(tileRow == tileY)) { -// let data = Data(bytes: dataQuery[tileData].bytes, count: dataQuery[tileData].bytes.count)// dataQuery![tileData].bytes -// result(data, nil) -// } else { -// Logger.services.error("No tile here: x:\(tileX) y:\(tileY) z:\(tileZ)") -// let error = NSError(domain: "LocalMBTileOverlay", code: 1, userInfo: ["reason": "no_tile"]) -// result(nil, error) -// } -// } -} - -// public class CustomMapOverlaySource: MKTileOverlay { -// -// // requires folder: tiles/{mapName}/z/y/y,{tileType} -// private var parent: MapViewSwiftUI -// private let mapName: String -// private let tileType: String -// private let defaultTile: DefaultTile? -// -// public init( -// parent: MapViewSwiftUI, -// mapName: String, -// tileType: String, -// defaultTile: DefaultTile? -// ) { -// self.parent = parent -// self.mapName = mapName -// self.tileType = tileType -// self.defaultTile = defaultTile -// super.init(urlTemplate: "") -// } -// -// public override func url(forTilePath path: MKTileOverlayPath) -> URL { -// if let tileUrl = Bundle.main.url( -// forResource: "\(path.y)", -// withExtension: self.tileType, -// subdirectory: "tiles/\(self.mapName)/\(path.z)/\(path.x)", -// localization: nil -// ) { -// return tileUrl -// } else if let defaultTile = self.defaultTile, let defaultTileUrl = Bundle.main.url( -// forResource: defaultTile.tileName, -// withExtension: defaultTile.tileType, -// subdirectory: "tiles/\(self.mapName)", -// localization: nil -// ) { -// return defaultTileUrl -// } else { -// let urlstring = self.mapName+"\(path.z)/\(path.x)/\(path.y).png" -// return URL(string: urlstring)! -// } -// } -// } From ad4dce42d476bd5c2cbf7921fdb7c7937a166457 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 19:01:17 -0700 Subject: [PATCH 022/333] readme updates --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f10d5531..982bd1fc 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,10 @@ open Meshtastic.xcworkspace ### Supported Operating Systems -* iOS 16+ -* iPadOS 16+ +The last two supported operating system versions are supported. Currently that is 16 and 17. + +* iOS 16.6+ +* iPadOS 16.6+ * macOS 13+ ### Code Standards From dc037fa34bca6350c2d836f7a03ea90a3f6b5bed Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 1 Aug 2024 19:01:59 -0700 Subject: [PATCH 023/333] don't be redundant --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 982bd1fc..de9af2a2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ open Meshtastic.xcworkspace ### Supported Operating Systems -The last two supported operating system versions are supported. Currently that is 16 and 17. +The last two operating system versions are supported. Currently that is 16 and 17. * iOS 16.6+ * iPadOS 16.6+ From bcdb5b94c4e100b011e144926249972870f9cfd1 Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Thu, 1 Aug 2024 21:34:19 -0500 Subject: [PATCH 024/333] Bump protos --- .../xcschemes/WidgetsExtension.xcscheme | 1 - .../Sources/meshtastic/admin.pb.swift | 261 ++-------- .../Sources/meshtastic/apponly.pb.swift | 6 +- .../Sources/meshtastic/atak.pb.swift | 64 +-- .../meshtastic/cannedmessages.pb.swift | 6 +- .../Sources/meshtastic/channel.pb.swift | 37 +- .../Sources/meshtastic/clientonly.pb.swift | 6 +- .../Sources/meshtastic/config.pb.swift | 426 ++++++--------- .../meshtastic/connection_status.pb.swift | 21 +- .../Sources/meshtastic/deviceonly.pb.swift | 33 +- .../Sources/meshtastic/localonly.pb.swift | 9 +- .../Sources/meshtastic/mesh.pb.swift | 490 ++++-------------- .../Sources/meshtastic/module_config.pb.swift | 263 +++------- .../Sources/meshtastic/mqtt.pb.swift | 9 +- .../Sources/meshtastic/paxcount.pb.swift | 6 +- .../Sources/meshtastic/portnums.pb.swift | 14 +- .../Sources/meshtastic/powermon.pb.swift | 115 ++-- .../meshtastic/remote_hardware.pb.swift | 35 +- .../Sources/meshtastic/rtttl.pb.swift | 6 +- .../Sources/meshtastic/storeforward.pb.swift | 93 +--- .../Sources/meshtastic/telemetry.pb.swift | 111 ++-- .../Sources/meshtastic/xmodem.pb.swift | 39 +- 22 files changed, 581 insertions(+), 1470 deletions(-) diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme index decd8381..880339bc 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme @@ -89,7 +89,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" - askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index ba263709..37528079 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -24,7 +24,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage { +public struct AdminMessage: 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. @@ -456,7 +456,7 @@ public struct AdminMessage { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -590,185 +590,11 @@ public struct AdminMessage { /// Tell the node to reset the nodedb. case nodedbReset(Int32) - #if !swift(>=4.1) - public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.getChannelRequest, .getChannelRequest): return { - guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getChannelResponse, .getChannelResponse): return { - guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerRequest, .getOwnerRequest): return { - guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerResponse, .getOwnerResponse): return { - guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigRequest, .getConfigRequest): return { - guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigResponse, .getConfigResponse): return { - guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigRequest, .getModuleConfigRequest): return { - guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigResponse, .getModuleConfigResponse): return { - guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { - guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { - guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { - guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { - guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneRequest, .getRingtoneRequest): return { - guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneResponse, .getRingtoneResponse): return { - guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { - guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { - guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setHamMode, .setHamMode): return { - guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { - guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { - guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.enterDfuModeRequest, .enterDfuModeRequest): return { - guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deleteFileRequest, .deleteFileRequest): return { - guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setScale, .setScale): return { - guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setOwner, .setOwner): return { - guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setChannel, .setChannel): return { - guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setConfig, .setConfig): return { - guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setModuleConfig, .setModuleConfig): return { - guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { - guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setRingtoneMessage, .setRingtoneMessage): return { - guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeByNodenum, .removeByNodenum): return { - guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFavoriteNode, .setFavoriteNode): return { - guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFavoriteNode, .removeFavoriteNode): return { - guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFixedPosition, .setFixedPosition): return { - guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFixedPosition, .removeFixedPosition): return { - guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.beginEditSettings, .beginEditSettings): return { - guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.commitEditSettings, .commitEditSettings): return { - guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootOtaSeconds, .rebootOtaSeconds): return { - guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.exitSimulator, .exitSimulator): return { - guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootSeconds, .rebootSeconds): return { - guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.shutdownSeconds, .shutdownSeconds): return { - guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryReset, .factoryReset): return { - guard case .factoryReset(let l) = lhs, case .factoryReset(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodedbReset, .nodedbReset): return { - guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum { + public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -830,11 +656,22 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + ] + } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum { + public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -932,50 +769,31 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] + } public init() {} } -#if swift(>=4.2) - -extension AdminMessage.ConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - ] -} - -extension AdminMessage.ModuleConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] -} - -#endif // swift(>=4.2) - /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters { +public struct HamParameters: 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. @@ -1005,7 +823,7 @@ public struct HamParameters { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse { +public struct NodeRemoteHardwarePinsResponse: 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. @@ -1019,15 +837,6 @@ public struct NodeRemoteHardwarePinsResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension AdminMessage: @unchecked Sendable {} -extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension AdminMessage.ConfigType: @unchecked Sendable {} -extension AdminMessage.ModuleConfigType: @unchecked Sendable {} -extension HamParameters: @unchecked Sendable {} -extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1725,7 +1534,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency != 0 { + if self.frequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 0457077c..18e66d8e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet { +public struct ChannelSet: 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. @@ -53,10 +53,6 @@ public struct ChannelSet { fileprivate var _loraConfig: Config.LoRaConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSet: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 4406deb3..1dd12469 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum { +public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -130,11 +130,6 @@ public enum Team: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -153,13 +148,12 @@ extension Team: CaseIterable { .darkGreen, .brown, ] -} -#endif // swift(>=4.2) +} /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum { +public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -233,11 +227,6 @@ public enum MemberRole: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -250,13 +239,12 @@ extension MemberRole: CaseIterable { .rto, .k9, ] -} -#endif // swift(>=4.2) +} /// /// Packets for the official ATAK Plugin -public struct TAKPacket { +public struct TAKPacket: 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. @@ -326,7 +314,7 @@ public struct TAKPacket { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TAK position report case pli(PLI) @@ -334,24 +322,6 @@ public struct TAKPacket { /// ATAK GeoChat message case chat(GeoChat) - #if !swift(>=4.1) - public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.pli, .pli): return { - guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.chat, .chat): return { - guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -363,7 +333,7 @@ public struct TAKPacket { /// /// ATAK GeoChat message -public struct GeoChat { +public struct GeoChat: 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. @@ -405,7 +375,7 @@ public struct GeoChat { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group { +public struct Group: 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. @@ -427,7 +397,7 @@ public struct Group { /// /// ATAK EUD Status /// -public struct Status { +public struct Status: 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. @@ -444,7 +414,7 @@ public struct Status { /// /// ATAK Contact /// -public struct Contact { +public struct Contact: 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. @@ -464,7 +434,7 @@ public struct Contact { /// /// Position Location Information from ATAK -public struct PLI { +public struct PLI: 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. @@ -496,18 +466,6 @@ public struct PLI { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Team: @unchecked Sendable {} -extension MemberRole: @unchecked Sendable {} -extension TAKPacket: @unchecked Sendable {} -extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension GeoChat: @unchecked Sendable {} -extension Group: @unchecked Sendable {} -extension Status: @unchecked Sendable {} -extension Contact: @unchecked Sendable {} -extension PLI: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..a43393e1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig { +public struct CannedMessageModuleConfig: 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. @@ -36,10 +36,6 @@ public struct CannedMessageModuleConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension CannedMessageModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index 5b9c7e49..a8c96595 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -36,13 +36,15 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings { +public struct ChannelSettings: @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. /// /// Deprecated in favor of LoraConfig.channel_num + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -111,7 +113,7 @@ public struct ChannelSettings { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings { +public struct ModuleSettings: 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. @@ -132,7 +134,7 @@ public struct ModuleSettings { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel { +public struct Channel: 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. @@ -170,7 +172,7 @@ public struct Channel { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -209,6 +211,13 @@ public struct Channel { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] + } public init() {} @@ -216,26 +225,6 @@ public struct Channel { fileprivate var _settings: ChannelSettings? = nil } -#if swift(>=4.2) - -extension Channel.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSettings: @unchecked Sendable {} -extension ModuleSettings: @unchecked Sendable {} -extension Channel: @unchecked Sendable {} -extension Channel.Role: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index c3d93bf7..89370cc5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile { +public struct DeviceProfile: 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. @@ -94,10 +94,6 @@ public struct DeviceProfile { fileprivate var _moduleConfig: LocalModuleConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceProfile: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index f396367c..aeaf9054 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config { +public struct Config: 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. @@ -89,7 +89,7 @@ public struct Config { /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -98,49 +98,11 @@ public struct Config { case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) - #if !swift(>=4.1) - public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.device, .device): return { - guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.position, .position): return { - guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.power, .power): return { - guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.network, .network): return { - guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.display, .display): return { - guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lora, .lora): return { - guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.bluetooth, .bluetooth): return { - guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// Configuration - public struct DeviceConfig { + public struct DeviceConfig: 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. @@ -202,7 +164,7 @@ public struct Config { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -220,6 +182,8 @@ public struct Config { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 + + /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -310,11 +274,26 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + ] + } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum { + public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -362,6 +341,14 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + ] + } public init() {} @@ -369,7 +356,7 @@ public struct Config { /// /// Position Config - public struct PositionConfig { + public struct PositionConfig: 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. @@ -391,6 +378,8 @@ public struct Config { /// /// Is GPS enabled for this node? + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -401,6 +390,8 @@ public struct Config { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -441,7 +432,7 @@ public struct Config { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum { + public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -531,9 +522,24 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] + } - public enum GpsMode: SwiftProtobuf.Enum { + public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -571,6 +577,13 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] + } public init() {} @@ -579,7 +592,7 @@ public struct Config { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig { + public struct PowerConfig: 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. @@ -639,7 +652,7 @@ public struct Config { /// /// Network Config - public struct NetworkConfig { + public struct NetworkConfig: 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. @@ -686,7 +699,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum { + public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -718,9 +731,15 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] + } - public struct IpV4Config { + public struct IpV4Config: 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. @@ -753,7 +772,7 @@ public struct Config { /// /// Display Config - public struct DisplayConfig { + public struct DisplayConfig: 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. @@ -809,7 +828,7 @@ public struct Config { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -872,11 +891,21 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] + } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum { + public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -908,11 +937,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] + } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum { + public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -956,9 +991,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + ] + } - public enum DisplayMode: SwiftProtobuf.Enum { + public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1002,9 +1045,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] + } - public enum CompassOrientation: SwiftProtobuf.Enum { + public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1072,6 +1123,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] + } public init() {} @@ -1079,7 +1142,7 @@ public struct Config { /// /// Lora Config - public struct LoRaConfig { + public struct LoRaConfig: @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. @@ -1236,7 +1299,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum { + public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1370,12 +1433,35 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + ] + } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum { + public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1443,6 +1529,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + ] + } public init() {} @@ -1450,7 +1548,7 @@ public struct Config { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig { + public struct BluetoothConfig: 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. @@ -1473,7 +1571,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum { + public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1511,6 +1609,13 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] + } public init() {} @@ -1519,199 +1624,6 @@ public struct Config { public init() {} } -#if swift(>=4.2) - -extension Config.DeviceConfig.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - ] -} - -extension Config.DeviceConfig.RebroadcastMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - ] -} - -extension Config.PositionConfig.PositionFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] -} - -extension Config.PositionConfig.GpsMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] -} - -extension Config.NetworkConfig.AddressMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] -} - -extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] -} - -extension Config.DisplayConfig.DisplayUnits: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] -} - -extension Config.DisplayConfig.OledType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - ] -} - -extension Config.DisplayConfig.DisplayMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] -} - -extension Config.DisplayConfig.CompassOrientation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] -} - -extension Config.LoRaConfig.RegionCode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - ] -} - -extension Config.LoRaConfig.ModemPreset: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - ] -} - -extension Config.BluetoothConfig.PairingMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension Config: @unchecked Sendable {} -extension Config.OneOf_PayloadVariant: @unchecked Sendable {} -extension Config.DeviceConfig: @unchecked Sendable {} -extension Config.DeviceConfig.Role: @unchecked Sendable {} -extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} -extension Config.PositionConfig: @unchecked Sendable {} -extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} -extension Config.PositionConfig.GpsMode: @unchecked Sendable {} -extension Config.PowerConfig: @unchecked Sendable {} -extension Config.NetworkConfig: @unchecked Sendable {} -extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} -extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} -extension Config.DisplayConfig: @unchecked Sendable {} -extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} -extension Config.DisplayConfig.OledType: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} -extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} -extension Config.LoRaConfig: @unchecked Sendable {} -extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} -extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} -extension Config.BluetoothConfig: @unchecked Sendable {} -extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2168,7 +2080,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride != 0 { + if self.adcMultiplierOverride.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2612,7 +2524,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset != 0 { + if _storage._frequencyOffset.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2636,7 +2548,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency != 0 { + if _storage._overrideFrequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a2ec180e..a4569714 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus { +public struct DeviceConnectionStatus: 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus { /// /// WiFi connection status -public struct WifiConnectionStatus { +public struct WifiConnectionStatus: 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus { /// /// Ethernet connection status -public struct EthernetConnectionStatus { +public struct EthernetConnectionStatus: 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus { +public struct NetworkConnectionStatus: 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus { +public struct BluetoothConnectionStatus: 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus { /// /// Serial connection status -public struct SerialConnectionStatus { +public struct SerialConnectionStatus: 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. @@ -209,15 +209,6 @@ public struct SerialConnectionStatus { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceConnectionStatus: @unchecked Sendable {} -extension WifiConnectionStatus: @unchecked Sendable {} -extension EthernetConnectionStatus: @unchecked Sendable {} -extension NetworkConnectionStatus: @unchecked Sendable {} -extension BluetoothConnectionStatus: @unchecked Sendable {} -extension SerialConnectionStatus: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 10b9af2b..834f9636 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum { +public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -60,24 +60,18 @@ public enum ScreenFonts: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, ] -} -#endif // swift(>=4.2) +} /// /// Position with static location information only for NodeDBLite -public struct PositionLite { +public struct PositionLite: 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. @@ -112,7 +106,7 @@ public struct PositionLite { public init() {} } -public struct NodeInfoLite { +public struct NodeInfoLite: @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. @@ -215,7 +209,7 @@ public struct NodeInfoLite { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState { +public struct DeviceState: @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. @@ -275,6 +269,8 @@ public struct DeviceState { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -323,7 +319,7 @@ public struct DeviceState { /// /// The on-disk saved channels -public struct ChannelFile { +public struct ChannelFile: 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. @@ -346,7 +342,7 @@ public struct ChannelFile { /// /// This can be used for customizing the firmware distribution. If populated, /// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore { +public struct OEMStore: @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. @@ -405,15 +401,6 @@ public struct OEMStore { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ScreenFonts: @unchecked Sendable {} -extension PositionLite: @unchecked Sendable {} -extension NodeInfoLite: @unchecked Sendable {} -extension DeviceState: @unchecked Sendable {} -extension ChannelFile: @unchecked Sendable {} -extension OEMStore: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -581,7 +568,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 5e30d1cd..17dd5baa 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig { +public struct LocalConfig: @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. @@ -118,7 +118,7 @@ public struct LocalConfig { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig { +public struct LocalModuleConfig: @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. @@ -282,11 +282,6 @@ public struct LocalModuleConfig { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=5.5) && canImport(_Concurrency) -extension LocalConfig: @unchecked Sendable {} -extension LocalModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 6a2906fe..996e8268 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -25,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum { +public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -496,11 +496,6 @@ public enum HardwareModel: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -576,13 +571,12 @@ extension HardwareModel: CaseIterable { .trackerT1000E, .privateHw, ] -} -#endif // swift(>=4.2) +} /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum { +public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -617,26 +611,20 @@ public enum Constants: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] -} -#endif // swift(>=4.2) +} /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum { +public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -730,11 +718,6 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -750,13 +733,12 @@ extension CriticalErrorCode: CaseIterable { .sx1262Failure, .radioSpiBug, ] -} -#endif // swift(>=4.2) +} /// /// a gps position -public struct Position { +public struct Position: @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. @@ -945,7 +927,7 @@ public struct Position { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum { + public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -989,12 +971,20 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] + } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum { + public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1044,6 +1034,15 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] + } public init() {} @@ -1051,31 +1050,6 @@ public struct Position { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension Position.LocSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] -} - -extension Position.AltSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] -} - -#endif // swift(>=4.2) - /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1097,7 +1071,7 @@ extension Position.AltSource: CaseIterable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User { +public struct User: @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. @@ -1122,6 +1096,8 @@ public struct User { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1148,7 +1124,7 @@ public struct User { /// /// A message used in our Dynamic Source Routing protocol (RFC 4728 based) -public struct RouteDiscovery { +public struct RouteDiscovery: 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. @@ -1164,7 +1140,7 @@ public struct RouteDiscovery { /// /// A Routing control Data packet handled by the routing module -public struct Routing { +public struct Routing: 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. @@ -1204,7 +1180,7 @@ public struct Routing { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1216,34 +1192,12 @@ public struct Routing { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) - #if !swift(>=4.1) - public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.routeRequest, .routeRequest): return { - guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.routeReply, .routeReply): return { - guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorReason, .errorReason): return { - guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum { + public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1337,38 +1291,32 @@ public struct Routing { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + ] + } public init() {} } -#if swift(>=4.2) - -extension Routing.Error: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - ] -} - -#endif // swift(>=4.2) - /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage { +public struct DataMessage: @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. @@ -1422,7 +1370,7 @@ public struct DataMessage { /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint { +public struct Waypoint: 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. @@ -1467,7 +1415,7 @@ public struct Waypoint { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage { +public struct MqttClientProxyMessage: @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. @@ -1508,7 +1456,7 @@ public struct MqttClientProxyMessage { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// Bytes case data(Data) @@ -1516,24 +1464,6 @@ public struct MqttClientProxyMessage { /// Text case text(String) - #if !swift(>=4.1) - public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.data, .data): return { - guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -1543,7 +1473,7 @@ public struct MqttClientProxyMessage { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket { +public struct MeshPacket: @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. @@ -1677,6 +1607,8 @@ public struct MeshPacket { /// /// Describe if this message is delayed + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -1699,7 +1631,7 @@ public struct MeshPacket { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1707,24 +1639,6 @@ public struct MeshPacket { /// TODO: REPLACE case encrypted(Data) - #if !swift(>=4.1) - public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.decoded, .decoded): return { - guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.encrypted, .encrypted): return { - guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// @@ -1746,7 +1660,7 @@ public struct MeshPacket { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum { + public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1811,11 +1725,22 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .ack, + .max, + ] + } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum { + public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1853,6 +1778,13 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] + } public init() {} @@ -1860,32 +1792,6 @@ public struct MeshPacket { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension MeshPacket.Priority: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .ack, - .max, - ] -} - -extension MeshPacket.Delayed: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] -} - -#endif // swift(>=4.2) - /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -1903,7 +1809,7 @@ extension MeshPacket.Delayed: CaseIterable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo { +public struct NodeInfo: @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. @@ -2004,7 +1910,7 @@ public struct NodeInfo { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo { +public struct MyNodeInfo: 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. @@ -2035,7 +1941,7 @@ public struct MyNodeInfo { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord { +public struct LogRecord: 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. @@ -2060,7 +1966,7 @@ public struct LogRecord { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum { + public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2122,29 +2028,23 @@ public struct LogRecord { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] + } public init() {} } -#if swift(>=4.2) - -extension LogRecord.Level: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] -} - -#endif // swift(>=4.2) - -public struct QueueStatus { +public struct QueueStatus: 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. @@ -2171,7 +2071,7 @@ public struct QueueStatus { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio { +public struct FromRadio: 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. @@ -2337,7 +2237,7 @@ public struct FromRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2389,72 +2289,6 @@ public struct FromRadio { /// File system manifest messages case fileInfo(FileInfo) - #if !swift(>=4.1) - public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.myInfo, .myInfo): return { - guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodeInfo, .nodeInfo): return { - guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.config, .config): return { - guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.logRecord, .logRecord): return { - guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.configCompleteID, .configCompleteID): return { - guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebooted, .rebooted): return { - guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.moduleConfig, .moduleConfig): return { - guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.channel, .channel): return { - guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.queueStatus, .queueStatus): return { - guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.metadata, .metadata): return { - guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileInfo, .fileInfo): return { - guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2462,7 +2296,7 @@ public struct FromRadio { /// /// Individual File info for the device -public struct FileInfo { +public struct FileInfo: 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. @@ -2483,7 +2317,7 @@ public struct FileInfo { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio { +public struct ToRadio: 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. @@ -2563,7 +2397,7 @@ public struct ToRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -2590,40 +2424,6 @@ public struct ToRadio { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) - #if !swift(>=4.1) - public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.wantConfigID, .wantConfigID): return { - guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.disconnect, .disconnect): return { - guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2631,7 +2431,7 @@ public struct ToRadio { /// /// Compressed message payload -public struct Compressed { +public struct Compressed: @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. @@ -2651,7 +2451,7 @@ public struct Compressed { /// /// Full info on edges for a single node -public struct NeighborInfo { +public struct NeighborInfo: 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. @@ -2679,7 +2479,7 @@ public struct NeighborInfo { /// /// A single edge in the mesh -public struct Neighbor { +public struct Neighbor: 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. @@ -2709,7 +2509,7 @@ public struct Neighbor { /// /// Device metadata response -public struct DeviceMetadata { +public struct DeviceMetadata: 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. @@ -2762,7 +2562,7 @@ public struct DeviceMetadata { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat { +public struct Heartbeat: 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. @@ -2774,7 +2574,7 @@ public struct Heartbeat { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin { +public struct NodeRemoteHardwarePin: 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. @@ -2801,7 +2601,7 @@ public struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload { +public struct ChunkedPayload: @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. @@ -2829,7 +2629,7 @@ public struct ChunkedPayload { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks { +public struct resend_chunks: 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. @@ -2843,7 +2643,7 @@ public struct resend_chunks { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse { +public struct ChunkedPayloadResponse: 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. @@ -2886,7 +2686,7 @@ public struct ChunkedPayloadResponse { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -2897,75 +2697,11 @@ public struct ChunkedPayloadResponse { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) - #if !swift(>=4.1) - public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.requestTransfer, .requestTransfer): return { - guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.acceptTransfer, .acceptTransfer): return { - guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.resendChunks, .resendChunks): return { - guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareModel: @unchecked Sendable {} -extension Constants: @unchecked Sendable {} -extension CriticalErrorCode: @unchecked Sendable {} -extension Position: @unchecked Sendable {} -extension Position.LocSource: @unchecked Sendable {} -extension Position.AltSource: @unchecked Sendable {} -extension User: @unchecked Sendable {} -extension RouteDiscovery: @unchecked Sendable {} -extension Routing: @unchecked Sendable {} -extension Routing.OneOf_Variant: @unchecked Sendable {} -extension Routing.Error: @unchecked Sendable {} -extension DataMessage: @unchecked Sendable {} -extension Waypoint: @unchecked Sendable {} -extension MqttClientProxyMessage: @unchecked Sendable {} -extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket: @unchecked Sendable {} -extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket.Priority: @unchecked Sendable {} -extension MeshPacket.Delayed: @unchecked Sendable {} -extension NodeInfo: @unchecked Sendable {} -extension MyNodeInfo: @unchecked Sendable {} -extension LogRecord: @unchecked Sendable {} -extension LogRecord.Level: @unchecked Sendable {} -extension QueueStatus: @unchecked Sendable {} -extension FromRadio: @unchecked Sendable {} -extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension FileInfo: @unchecked Sendable {} -extension ToRadio: @unchecked Sendable {} -extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension Compressed: @unchecked Sendable {} -extension NeighborInfo: @unchecked Sendable {} -extension Neighbor: @unchecked Sendable {} -extension DeviceMetadata: @unchecked Sendable {} -extension Heartbeat: @unchecked Sendable {} -extension NodeRemoteHardwarePin: @unchecked Sendable {} -extension ChunkedPayload: @unchecked Sendable {} -extension resend_chunks: @unchecked Sendable {} -extension ChunkedPayloadResponse: @unchecked Sendable {} -extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -3911,7 +3647,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr != 0 { + if _storage._rxSnr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -4086,7 +3822,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -4859,7 +4595,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr != 0 { + if self.snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -4972,8 +4708,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index c68ffd83..6f3b2d76 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +58,18 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] -} -#endif // swift(>=4.2) +} /// /// Module Config -public struct ModuleConfig { +public struct ModuleConfig: 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. @@ -218,7 +212,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -259,73 +253,11 @@ public struct ModuleConfig { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) - #if !swift(>=4.1) - public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.mqtt, .mqtt): return { - guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.serial, .serial): return { - guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.externalNotification, .externalNotification): return { - guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeForward, .storeForward): return { - guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rangeTest, .rangeTest): return { - guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.telemetry, .telemetry): return { - guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.cannedMessage, .cannedMessage): return { - guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.audio, .audio): return { - guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.remoteHardware, .remoteHardware): return { - guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.neighborInfo, .neighborInfo): return { - guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.ambientLighting, .ambientLighting): return { - guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detectionSensor, .detectionSensor): return { - guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.paxcounter, .paxcounter): return { - guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// MQTT Client Config - public struct MQTTConfig { + public struct MQTTConfig: 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. @@ -400,7 +332,7 @@ public struct ModuleConfig { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings { + public struct MapReportSettings: 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. @@ -420,7 +352,7 @@ public struct ModuleConfig { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig { + public struct RemoteHardwareConfig: 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. @@ -444,7 +376,7 @@ public struct ModuleConfig { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig { + public struct NeighborInfoConfig: 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. @@ -465,7 +397,7 @@ public struct ModuleConfig { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig { + public struct DetectionSensorConfig: 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. @@ -516,7 +448,7 @@ public struct ModuleConfig { /// /// Audio Config for codec2 voice - public struct AudioConfig { + public struct AudioConfig: 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. @@ -553,7 +485,7 @@ public struct ModuleConfig { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum { + public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -600,6 +532,19 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] + } public init() {} @@ -607,7 +552,7 @@ public struct ModuleConfig { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig { + public struct PaxcounterConfig: 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. @@ -633,7 +578,7 @@ public struct ModuleConfig { /// /// Serial Config - public struct SerialConfig { + public struct SerialConfig: 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. @@ -676,7 +621,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum { + public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -744,11 +689,31 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] + } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum { + public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -793,6 +758,17 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + ] + } public init() {} @@ -800,7 +776,7 @@ public struct ModuleConfig { /// /// External Notifications Config - public struct ExternalNotificationConfig { + public struct ExternalNotificationConfig: 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. @@ -883,7 +859,7 @@ public struct ModuleConfig { /// /// Store and Forward Module Config - public struct StoreForwardConfig { + public struct StoreForwardConfig: 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. @@ -919,7 +895,7 @@ public struct ModuleConfig { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig { + public struct RangeTestConfig: 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. @@ -944,7 +920,7 @@ public struct ModuleConfig { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig { + public struct TelemetryConfig: 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. @@ -1001,7 +977,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public struct CannedMessageConfig { + public struct CannedMessageConfig: 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. @@ -1056,7 +1032,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1124,6 +1100,18 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] + } public init() {} @@ -1132,7 +1120,7 @@ public struct ModuleConfig { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig { + public struct AmbientLightingConfig: 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. @@ -1165,77 +1153,9 @@ public struct ModuleConfig { public init() {} } -#if swift(>=4.2) - -extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] -} - -extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] -} - -#endif // swift(>=4.2) - /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin { +public struct RemoteHardwarePin: 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. @@ -1257,31 +1177,6 @@ public struct RemoteHardwarePin { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RemoteHardwarePinType: @unchecked Sendable {} -extension ModuleConfig: @unchecked Sendable {} -extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} -extension ModuleConfig.MQTTConfig: @unchecked Sendable {} -extension ModuleConfig.MapReportSettings: @unchecked Sendable {} -extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} -extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} -extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} -extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} -extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} -extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} -extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} -extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} -extension RemoteHardwarePin: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index efe6cdd5..fc5e37a1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope { +public struct ServiceEnvelope: 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport { +public struct MapReport: 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. @@ -121,11 +121,6 @@ public struct MapReport { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension ServiceEnvelope: @unchecked Sendable {} -extension MapReport: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index cf8aa463..f82b3c51 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount { +public struct Paxcount: 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. @@ -44,10 +44,6 @@ public struct Paxcount { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Paxcount: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index c728c961..c5348a8a 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum { +public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -277,11 +277,6 @@ public enum PortNum: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -313,14 +308,9 @@ extension PortNum: CaseIterable { .atakForwarder, .max, ] + } -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PortNum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 5f51e948..9c61e6d0 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon { +public struct PowerMon: 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. @@ -31,7 +31,7 @@ public struct PowerMon { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum { + public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,37 +104,31 @@ public struct PowerMon { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerMon.State: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] -} - -#endif // swift(>=4.2) - /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage { +public struct PowerStressMessage: 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. @@ -151,7 +145,7 @@ public struct PowerStressMessage { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum { + public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -272,48 +266,35 @@ public struct PowerStressMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerStressMessage.Opcode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PowerMon: @unchecked Sendable {} -extension PowerMon.State: @unchecked Sendable {} -extension PowerStressMessage: @unchecked Sendable {} -extension PowerStressMessage.Opcode: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -323,8 +304,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -379,7 +360,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds != 0 { + if self.numSeconds.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index ac6eeb26..60f64504 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage { +public struct HardwareMessage: 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. @@ -52,7 +52,7 @@ public struct HardwareMessage { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum { + public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -110,32 +110,21 @@ public struct HardwareMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] + } public init() {} } -#if swift(>=4.2) - -extension HardwareMessage.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareMessage: @unchecked Sendable {} -extension HardwareMessage.TypeEnum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index 6fdf3208..c1f3f678 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig { +public struct RTTTLConfig: 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. @@ -36,10 +36,6 @@ public struct RTTTLConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RTTTLConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 54efa77b..0b67eaf6 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward { +public struct StoreAndForward: @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. @@ -79,7 +79,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,38 +93,12 @@ public struct StoreAndForward { /// Text from history message. case text(Data) - #if !swift(>=4.1) - public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.stats, .stats): return { - guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.history, .history): return { - guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum { + public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -242,11 +216,31 @@ public struct StoreAndForward { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] + } /// /// TODO: REPLACE - public struct Statistics { + public struct Statistics: 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. @@ -294,7 +288,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct History { + public struct History: 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. @@ -319,7 +313,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct Heartbeat { + public struct Heartbeat: 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. @@ -340,41 +334,6 @@ public struct StoreAndForward { public init() {} } -#if swift(>=4.2) - -extension StoreAndForward.RequestResponse: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension StoreAndForward: @unchecked Sendable {} -extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} -extension StoreAndForward.RequestResponse: @unchecked Sendable {} -extension StoreAndForward.Statistics: @unchecked Sendable {} -extension StoreAndForward.History: @unchecked Sendable {} -extension StoreAndForward.Heartbeat: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index ec627e3d..7a9b81de 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum { +public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -198,11 +198,6 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -232,13 +227,12 @@ extension TelemetrySensorType: CaseIterable { .dfrobotLark, .nau7802, ] -} -#endif // swift(>=4.2) +} /// /// Key native device metrics such as battery level -public struct DeviceMetrics { +public struct DeviceMetrics: 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. @@ -270,7 +264,7 @@ public struct DeviceMetrics { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics { +public struct EnvironmentMetrics: @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. @@ -405,7 +399,7 @@ public struct EnvironmentMetrics { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics { +public struct PowerMetrics: 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. @@ -441,7 +435,7 @@ public struct PowerMetrics { /// /// Air quality metrics -public struct AirQualityMetrics { +public struct AirQualityMetrics: 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. @@ -501,7 +495,7 @@ public struct AirQualityMetrics { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry { +public struct Telemetry: 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. @@ -554,7 +548,7 @@ public struct Telemetry { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -568,32 +562,6 @@ public struct Telemetry { /// Power Metrics case powerMetrics(PowerMetrics) - #if !swift(>=4.1) - public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.deviceMetrics, .deviceMetrics): return { - guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.environmentMetrics, .environmentMetrics): return { - guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.airQualityMetrics, .airQualityMetrics): return { - guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.powerMetrics, .powerMetrics): return { - guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -601,7 +569,7 @@ public struct Telemetry { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config { +public struct Nau7802Config: 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. @@ -619,17 +587,6 @@ public struct Nau7802Config { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension TelemetrySensorType: @unchecked Sendable {} -extension DeviceMetrics: @unchecked Sendable {} -extension EnvironmentMetrics: @unchecked Sendable {} -extension PowerMetrics: @unchecked Sendable {} -extension AirQualityMetrics: @unchecked Sendable {} -extension Telemetry: @unchecked Sendable {} -extension Telemetry.OneOf_Variant: @unchecked Sendable {} -extension Nau7802Config: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -695,13 +652,13 @@ extension DeviceMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.batteryLevel != 0 { try visitor.visitSingularUInt32Field(value: self.batteryLevel, fieldNumber: 1) } - if self.voltage != 0 { + if self.voltage.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.voltage, fieldNumber: 2) } - if self.channelUtilization != 0 { + if self.channelUtilization.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 3) } - if self.airUtilTx != 0 { + if self.airUtilTx.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 4) } if self.uptimeSeconds != 0 { @@ -835,55 +792,55 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._temperature != 0 { + if _storage._temperature.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._temperature, fieldNumber: 1) } - if _storage._relativeHumidity != 0 { + if _storage._relativeHumidity.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._relativeHumidity, fieldNumber: 2) } - if _storage._barometricPressure != 0 { + if _storage._barometricPressure.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._barometricPressure, fieldNumber: 3) } - if _storage._gasResistance != 0 { + if _storage._gasResistance.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._gasResistance, fieldNumber: 4) } - if _storage._voltage != 0 { + if _storage._voltage.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._voltage, fieldNumber: 5) } - if _storage._current != 0 { + if _storage._current.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._current, fieldNumber: 6) } if _storage._iaq != 0 { try visitor.visitSingularUInt32Field(value: _storage._iaq, fieldNumber: 7) } - if _storage._distance != 0 { + if _storage._distance.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._distance, fieldNumber: 8) } - if _storage._lux != 0 { + if _storage._lux.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._lux, fieldNumber: 9) } - if _storage._whiteLux != 0 { + if _storage._whiteLux.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._whiteLux, fieldNumber: 10) } - if _storage._irLux != 0 { + if _storage._irLux.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._irLux, fieldNumber: 11) } - if _storage._uvLux != 0 { + if _storage._uvLux.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._uvLux, fieldNumber: 12) } if _storage._windDirection != 0 { try visitor.visitSingularUInt32Field(value: _storage._windDirection, fieldNumber: 13) } - if _storage._windSpeed != 0 { + if _storage._windSpeed.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._windSpeed, fieldNumber: 14) } - if _storage._weight != 0 { + if _storage._weight.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._weight, fieldNumber: 15) } - if _storage._windGust != 0 { + if _storage._windGust.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._windGust, fieldNumber: 16) } - if _storage._windLull != 0 { + if _storage._windLull.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._windLull, fieldNumber: 17) } } @@ -950,22 +907,22 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } public func traverse(visitor: inout V) throws { - if self.ch1Voltage != 0 { + if self.ch1Voltage.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.ch1Voltage, fieldNumber: 1) } - if self.ch1Current != 0 { + if self.ch1Current.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.ch1Current, fieldNumber: 2) } - if self.ch2Voltage != 0 { + if self.ch2Voltage.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.ch2Voltage, fieldNumber: 3) } - if self.ch2Current != 0 { + if self.ch2Current.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.ch2Current, fieldNumber: 4) } - if self.ch3Voltage != 0 { + if self.ch3Voltage.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.ch3Voltage, fieldNumber: 5) } - if self.ch3Current != 0 { + if self.ch3Current.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.ch3Current, fieldNumber: 6) } try unknownFields.traverse(visitor: &visitor) @@ -1217,7 +1174,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor != 0 { + if self.calibrationFactor.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 1f41fe0b..89d0097c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem { +public struct XModem: @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. @@ -35,7 +35,7 @@ public struct XModem { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum { + public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,34 +79,23 @@ public struct XModem { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] + } public init() {} } -#if swift(>=4.2) - -extension XModem.Control: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension XModem: @unchecked Sendable {} -extension XModem.Control: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" From 75b6b4e6afc0d6d1c4694bf35d0733e6e2469788 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 2 Aug 2024 12:18:41 -0700 Subject: [PATCH 025/333] Restore node list context menu logic, alerts, delays and confirmation dialog --- Localizable.xcstrings | 15 ++++ Meshtastic/Views/Nodes/NodeList.swift | 110 +++++++++++++++++++++----- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 967567c0..e33d88ed 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7179,6 +7179,9 @@ }, "Factory reset your device and app? " : { + }, + "Failed to get a valid position to exchange" : { + }, "Favorite" : { @@ -16426,6 +16429,12 @@ } } } + }, + "Position Exchange Failed" : { + + }, + "Position Exchange Requested" : { + }, "Position Flags" : { @@ -20928,6 +20937,9 @@ }, "This conversation will be deleted." : { + }, + "This could take a while, response will appear in the trace route log for the node it was sent to." : { + }, "This could take a while. The response will appear in the trace route log for the node it was sent to." : { @@ -22491,6 +22503,9 @@ }, "Your position has been sent with a request for a response with their position." : { + }, + "Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : { + }, "Your region has a %lld%% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh." : { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index ac9aa65e..3773593b 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -31,6 +31,12 @@ struct NodeList: View { @State private var hopsAway: Double = -1.0 @State private var roleFilter = false @State private var deviceRoles: Set = [] + + @State private var isPresentingTraceRouteSentAlert = false + @State private var isPresentingPositionSentAlert = false + @State private var isPresentingPositionFailedAlert = false + @State private var isPresentingDeleteNodeAlert = false + @State private var deleteNodeId: Int64 = 0 var boolFilters: [Bool] {[ isOnline, @@ -66,12 +72,7 @@ struct NodeList: View { node: NodeInfoEntity, connectedNode: NodeInfoEntity? ) -> some View { - FavoriteNodeButton( - bleManager: bleManager, - context: context, - node: node - ) - + /// Allow users to mute notifications for a node even if they are not connected if let user = node.user { NodeAlertsButton( context: context, @@ -79,22 +80,57 @@ struct NodeList: View { user: user ) } - if let connectedNode { - ExchangePositionsButton( + /// Favoriting a node requires being connected + FavoriteNodeButton( bleManager: bleManager, + context: context, node: node ) - TraceRouteButton( - bleManager: bleManager, - node: node - ) -// DeleteNodeButton( -// bleManager: bleManager, -// context: context, -// connectedNode: connectedNode, -// node: node -// ) + /// Don't show trace route, position exchange or delete context menu items for the connected node + if connectedNode.num != node.num { + Button { + let traceRouteSent = bleManager.sendTraceRouteRequest( + destNum: node.num, + wantResponse: true + ) + if traceRouteSent { + isPresentingTraceRouteSentAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingTraceRouteSentAlert = false + } + } + + } label: { + Label("Trace Route", systemImage: "signpost.right.and.left") + } + Button { + let positionSent = bleManager.sendPosition( + channel: node.channel, + destNum: node.num, + wantResponse: true + ) + if positionSent { + isPresentingPositionSentAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingPositionSentAlert = false + } + } else { + isPresentingPositionFailedAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingPositionFailedAlert = false + } + } + } label: { + Label("Exchange Positions", systemImage: "arrow.triangle.2.circlepath") + } + Button(role: .destructive) { + deleteNodeId = node.num + isPresentingDeleteNodeAlert = true + } label: { + Label("Delete Node", systemImage: "trash") + } + } } } @@ -150,6 +186,44 @@ struct NodeList: View { .scrollDismissesKeyboard(.immediately) .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) + .alert( + "Position Exchange Requested", + isPresented: $isPresentingPositionSentAlert) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned.") + } + .alert( + "Position Exchange Failed", + isPresented: $isPresentingPositionFailedAlert) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Failed to get a valid position to exchange") + } + .alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("This could take a while, response will appear in the trace route log for the node it was sent to.") + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingDeleteNodeAlert, + titleVisibility: .visible + ) { + Button("Delete Node") { + let deleteNode = getNodeInfo(id: deleteNodeId, context: context) + if connectedNode != nil { + if deleteNode != nil { + let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(bleManager.connectedPeripheral?.num ?? -1)) + if !success { + Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)") + } + } + } + } + } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems( leading: MeshtasticLogo(), From 7ea68d8e228bfc9488968eb2eaf4f420e87b4387 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 2 Aug 2024 12:32:22 -0700 Subject: [PATCH 026/333] Add actions logic to node details --- .../Actions/ExchangePositionsButton.swift | 25 ++++++++++++++-- .../Views/Nodes/Helpers/NodeDetail.swift | 29 ++++++++----------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index ed5fce47..eccb6b97 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -6,16 +6,28 @@ struct ExchangePositionsButton: View { var node: NodeInfoEntity - @State - private var isPresentingPositionSentAlert: Bool = false + @State private var isPresentingPositionSentAlert: Bool = false + @State private var isPresentingPositionFailedAlert: Bool = false var body: some View { Button { - isPresentingPositionSentAlert = bleManager.sendPosition( + let positionSent = bleManager.sendPosition( channel: node.channel, destNum: node.num, wantResponse: true ) + if positionSent { + isPresentingPositionSentAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingPositionSentAlert = false + } + } else { + isPresentingPositionFailedAlert = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + isPresentingPositionFailedAlert = false + } + } + } label: { Label { Text("Exchange Positions") @@ -30,6 +42,13 @@ struct ExchangePositionsButton: View { Button("OK") { }.keyboardShortcut(.defaultAction) } message: { Text("Your position has been sent with a request for a response with their position.") + }.alert( + "Position Exchange Failed", + isPresented: $isPresentingPositionFailedAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Failed to get a valid position to exchange.") } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index fc9b44c1..9bc5781a 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -281,12 +281,6 @@ struct NodeDetail: View { } Section("Actions") { - FavoriteNodeButton( - bleManager: bleManager, - context: context, - node: node - ) - if let user = node.user { NodeAlertsButton( context: context, @@ -295,19 +289,21 @@ struct NodeDetail: View { ) } - if let connectedPeripheral = bleManager.connectedPeripheral, - node.num != connectedPeripheral.num { - ExchangePositionsButton( + if let connectedNode { + FavoriteNodeButton( bleManager: bleManager, + context: context, node: node ) - - TraceRouteButton( - bleManager: bleManager, - node: node - ) - - if let connectedNode { + if connectedNode.num != node.num { + ExchangePositionsButton( + bleManager: bleManager, + node: node + ) + TraceRouteButton( + bleManager: bleManager, + node: node + ) if node.isStoreForwardRouter { ClientHistoryButton( bleManager: bleManager, @@ -315,7 +311,6 @@ struct NodeDetail: View { node: node ) } - DeleteNodeButton( bleManager: bleManager, context: context, From 350fe20015c9495fe86b1364462b983d93864ff6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 2 Aug 2024 16:13:22 -0700 Subject: [PATCH 027/333] Update exchange position alert text --- Localizable.xcstrings | 6 +++--- .../Nodes/Helpers/Actions/ExchangePositionsButton.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e33d88ed..a5328fca 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7182,6 +7182,9 @@ }, "Failed to get a valid position to exchange" : { + }, + "Failed to get a valid position to exchange." : { + }, "Favorite" : { @@ -22500,9 +22503,6 @@ }, "Your MQTT Server must support TLS." : { - }, - "Your position has been sent with a request for a response with their position." : { - }, "Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : { diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index eccb6b97..f98bc999 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -41,7 +41,7 @@ struct ExchangePositionsButton: View { ) { Button("OK") { }.keyboardShortcut(.defaultAction) } message: { - Text("Your position has been sent with a request for a response with their position.") + Text("Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned.") }.alert( "Position Exchange Failed", isPresented: $isPresentingPositionFailedAlert From acea2603261c10afcc27ead3efabed697fbdcd44 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 2 Aug 2024 20:45:25 -0700 Subject: [PATCH 028/333] Select the chart by the second --- Localizable.xcstrings | 6 +++--- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 967567c0..0bc052fb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -14631,9 +14631,6 @@ }, "Mininum time between detection broadcasts. Default is 45 seconds." : { - }, - "Minute" : { - }, "mode" : { "localizations" : { @@ -18789,6 +18786,9 @@ }, "Search" : { + }, + "Second" : { + }, "Secondary" : { diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index d62d62c0..3ffce6cb 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -60,7 +60,7 @@ struct DeviceMetricsLog: View { .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") .foregroundStyle(channelUtilizationChartColor) if let chartSelection { - RuleMark(x: .value("Minute", chartSelection, unit: .minute)) + RuleMark(x: .value("Second", chartSelection, unit: .second)) .foregroundStyle(.tertiary.opacity(0.5)) // .annotation( // position: .automatic, From f24a14bd1985395952e5abd6f462c6004ea716e9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 3 Aug 2024 07:02:51 -0700 Subject: [PATCH 029/333] Sort Descriptors --- Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift | 1 + Meshtastic/Extensions/CoreData/MessageEntityExtension.swift | 3 ++- Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift | 1 + Meshtastic/Extensions/CoreData/UserEntityExtension.swift | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 2ff2746c..8f35002f 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -13,6 +13,7 @@ extension ChannelEntity { var allPrivateMessages: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index db3a0899..18d97f6c 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -14,7 +14,7 @@ import SwiftUI extension MessageEntity { var timestamp: Date { - let time = messageTimestamp + let time = messageTimestamp return Date(timeIntervalSince1970: TimeInterval(time)) } @@ -25,6 +25,7 @@ extension MessageEntity { var tapbacks: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] fetchRequest.predicate = NSPredicate(format: "replyID == %lld AND isEmoji == true", self.messageId) return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index d2172400..7b07575b 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -12,6 +12,7 @@ extension MyInfoEntity { var messageList: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] fetchRequest.predicate = NSPredicate(format: "toUser == nil") return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index c91ce34b..8c49b322 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -14,6 +14,7 @@ extension UserEntity { var messageList: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] fetchRequest.predicate = NSPredicate(format: "((toUser == %@) OR (fromUser == %@)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10", self, self) return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() @@ -22,6 +23,7 @@ extension UserEntity { var sensorMessageList: [MessageEntity] { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] fetchRequest.predicate = NSPredicate(format: "(fromUser == %@) AND portNum = 10", self) return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() From 77739ce0f17e260bdbaa152d2fb0fee5d688a46d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 3 Aug 2024 16:11:11 -0700 Subject: [PATCH 030/333] Remove padding from node hardware battery indicator, bump rssi and snr font back up to caption --- Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index e1626b6b..b7fee092 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -32,11 +32,11 @@ struct NodeInfoItem: View { Image(user.hardwareImage ?? "UNSET") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 75, height: 75) + .frame(width: 65, height: 65) .cornerRadius(5) Text(String(node.user!.hwModel ?? "unset".localized)) .font(.caption2) - .frame(maxWidth: 100) + .frame(maxWidth: 80) } else { Image(systemName: "person.crop.circle.badge.questionmark") .resizable() @@ -57,20 +57,18 @@ struct NodeInfoItem: View { Text("Signal \(signalStrength.description)").font(.footnote) Text("SNR \(String(format: "%.2f", node.snr))dB") .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) - .font(.caption2) + .font(.caption) Text("RSSI \(node.rssi)dB") .foregroundColor(getRssiColor(rssi: node.rssi)) - .font(.caption2) + .font(.caption) } - .frame(minWidth: 100, maxWidth: 140) + .frame(minWidth: 110, maxWidth: 175) } if node.telemetries?.count ?? 0 > 0 { BatteryGauge(node: node) - .padding() } Spacer() } - .padding(.leading) } } From e4470375dce93a64de3fc7dfcca045bcda77fbef Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 3 Aug 2024 16:14:18 -0700 Subject: [PATCH 031/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index dca5ef1c..8e478691 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1600,7 +1600,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.1; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1635,7 +1635,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.1; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1667,7 +1667,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.1; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1700,7 +1700,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.1; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 829f1f8a256a6fcd7ee700a6b805880d0e762bc3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 3 Aug 2024 18:52:25 -0700 Subject: [PATCH 032/333] Smaller snr font --- Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index b7fee092..1355ce57 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -57,7 +57,7 @@ struct NodeInfoItem: View { Text("Signal \(signalStrength.description)").font(.footnote) Text("SNR \(String(format: "%.2f", node.snr))dB") .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) - .font(.caption) + .font(.caption2) Text("RSSI \(node.rssi)dB") .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) From 257ec71a9389f02ec27866724dfb4c10cd99a25c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 13:13:02 -0700 Subject: [PATCH 033/333] True bearing for the node list --- Meshtastic.xcodeproj/project.pbxproj | 4 +++ Meshtastic/Extensions/CLLocation.swift | 28 +++++++++++++++++++ .../Views/Nodes/Helpers/NodeListItem.swift | 22 +++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 Meshtastic/Extensions/CLLocation.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index dca5ef1c..aa35c3b9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */; }; DD1933782B084F4200771CD5 /* Measurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933772B084F4200771CD5 /* Measurement.swift */; }; DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */; }; + DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -287,6 +288,7 @@ DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAltitudeChart.swift; sourceTree = ""; }; DD1933772B084F4200771CD5 /* Measurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurement.swift; sourceTree = ""; }; DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSStatus.swift; sourceTree = ""; }; + DD1BD0EA2C601795008C0C70 /* CLLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocation.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -962,6 +964,7 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */, DDFFA7462B3A7F3C004730DB /* Bundle.swift */, DDDB444529F8A96500EE2349 /* Character.swift */, + DD1BD0EA2C601795008C0C70 /* CLLocation.swift */, DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */, DDDB444B29F8AAA600EE2349 /* Color.swift */, 25C49D8F2C471AEA0024FBD1 /* Constants.swift */, @@ -1269,6 +1272,7 @@ DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */, DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, + DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, diff --git a/Meshtastic/Extensions/CLLocation.swift b/Meshtastic/Extensions/CLLocation.swift new file mode 100644 index 00000000..c4a81849 --- /dev/null +++ b/Meshtastic/Extensions/CLLocation.swift @@ -0,0 +1,28 @@ +// +// CLLocation.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/4/24. +// +import Foundation +import MapKit + +func degreesToRadians(degrees: Double) -> Double { return degrees * .pi / 180.0 } +func radiansToDegrees(radians: Double) -> Double { return radians * 180.0 / .pi } + +func getBearingBetweenTwoPoints(point1: CLLocation, point2: CLLocation) -> Double { + + let lat1 = degreesToRadians(degrees: point1.coordinate.latitude) + let lon1 = degreesToRadians(degrees: point1.coordinate.longitude) + + let lat2 = degreesToRadians(degrees: point2.coordinate.latitude) + let lon2 = degreesToRadians(degrees: point2.coordinate.longitude) + + let dLon = lon2 - lon1 + + let y = sin(dLon) * cos(lat2) + let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon) + let radiansBearing = atan2(y, x) + + return radiansToDegrees(radians: radiansBearing) +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a68a63cd..70762e27 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -99,6 +99,17 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let headingDegrees = Angle.degrees(trueBearing) + Image(systemName: "location.north.circle.fill") + .font(.caption) + .symbolRenderingMode(.multicolor) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) } } } else { @@ -114,6 +125,17 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.secondary) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let headingDegrees = Angle.degrees(trueBearing) + Image(systemName: "location.north.circle.fill") + .font(.caption) + .symbolRenderingMode(.multicolor) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) } } } From d9547c4362b1a649ee38d4597081742ee84c9bf1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 15:53:59 -0700 Subject: [PATCH 034/333] little feets --- Localizable.xcstrings | 3 - Meshtastic.xcodeproj/project.pbxproj | 12 +++ .../CoreData/NodeInfoEntityExtension.swift | 2 +- Meshtastic/Measurement/CustomFormatters.swift | 18 ++++ Meshtastic/MeshtasticApp.swift | 2 +- .../Nodes/Helpers/Map/PositionPopover.swift | 14 ++- .../Views/Nodes/Helpers/NodeDetail.swift | 2 +- .../Views/Nodes/Helpers/NodeInfoItem.swift | 92 +++++++++---------- .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- 9 files changed, 92 insertions(+), 55 deletions(-) create mode 100644 Meshtastic/Measurement/CustomFormatters.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 37750d4d..d80208dd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -723,9 +723,6 @@ }, "Altitude is Mean Sea Level" : { - }, - "Altitude: %@" : { - }, "Always point north" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 204e3b63..0e490c66 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ DD1933782B084F4200771CD5 /* Measurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933772B084F4200771CD5 /* Measurement.swift */; }; DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */; }; DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; + DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -289,6 +290,7 @@ DD1933772B084F4200771CD5 /* Measurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurement.swift; sourceTree = ""; }; DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSStatus.swift; sourceTree = ""; }; DD1BD0EA2C601795008C0C70 /* CLLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocation.swift; sourceTree = ""; }; + DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -593,6 +595,14 @@ path = CoreData; sourceTree = ""; }; + DD1BD0EC2C603C5B008C0C70 /* Measurement */ = { + isa = PBXGroup; + children = ( + DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */, + ); + path = Measurement; + sourceTree = ""; + }; DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( @@ -803,6 +813,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, DD7709392AA1ABA1007A8BF0 /* Tips */, DD90860A26F645B700DC5189 /* Meshtastic.entitlements */, @@ -1308,6 +1319,7 @@ DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, + DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */, DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD97E96828EFE9A00056DDA4 /* About.swift in Sources */, diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index e88a2bee..66c915df 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -63,7 +63,7 @@ public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeI newNode.num = Int64(num) let newUser = UserEntity(context: context) newUser.num = Int64(num) - let userId = String(format: "%2X", num) + let userId = num.toHex() newUser.userId = "!\(userId)" let last4 = String(userId.suffix(4)) newUser.longName = "Meshtastic \(last4)" diff --git a/Meshtastic/Measurement/CustomFormatters.swift b/Meshtastic/Measurement/CustomFormatters.swift new file mode 100644 index 00000000..e14c96fd --- /dev/null +++ b/Meshtastic/Measurement/CustomFormatters.swift @@ -0,0 +1,18 @@ +// +// CustomFormatters.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 8/4/24. +// + +import Foundation + +/// Custom altitude formatter that always returns the provided unit +/// Needs to be used in conjunction with logic that checks for metric and displays the right value. +public var altitudeFormatter: MeasurementFormatter { + let formatter = MeasurementFormatter() + formatter.unitOptions = .providedUnit + formatter.unitStyle = .long + formatter.numberFormatter.maximumFractionDigits = 1 + return formatter +} diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index ff691440..bea45fa6 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -93,7 +93,7 @@ struct MeshtasticAppleApp: App { if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false - if ((self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil) { + if (((self.incomingUrl?.absoluteString.lowercased().contains("?"))) != nil) { guard let cs = components.last!.components(separatedBy: "?").first else { return } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index faa49760..b3b9c18e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -10,6 +10,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct PositionPopover: View { + @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -74,8 +75,17 @@ struct PositionPopover: View { .padding(.bottom, 5) /// Altitude Label { - Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))") - .foregroundColor(.primary) + let formatter = MeasurementFormatter() + let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters) + let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet) + if Locale.current.measurementSystem == .metric { + Text(altitudeFormatter.string(from: distanceInMeters)) + .foregroundColor(.primary) + } else { + Text(altitudeFormatter.string(from: distanceInFeet)) + .foregroundColor(.primary) + } + } icon: { Image(systemName: "mountain.2.fill") .symbolRenderingMode(.hierarchical) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 9bc5781a..4bf38ec7 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -65,7 +65,7 @@ struct NodeDetail: View { .symbolRenderingMode(.multicolor) } Spacer() - Text(node.user?.userId ?? "?") + Text(node.num.toHex()) .textSelection(.enabled) } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 1355ce57..0335a6fd 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -19,56 +19,56 @@ struct NodeInfoItem: View { ) ?? ModemPresets.longFast var body: some View { - HStack { - Spacer() - CircleText( - text: node.user?.shortName ?? "?", - color: Color(UIColor(hex: UInt32(node.num))), - circleSize: 65 - ) - if let user = node.user { - VStack(alignment: .center) { - if user.hwModel != "UNSET" { - Image(user.hardwareImage ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 65, height: 65) - .cornerRadius(5) - Text(String(node.user!.hwModel ?? "unset".localized)) - .font(.caption2) - .frame(maxWidth: 80) - } else { - Image(systemName: "person.crop.circle.badge.questionmark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 65, height: 65) - .cornerRadius(5) - Text(String("incomplete".localized)) - .font(.caption) - .frame(maxWidth: 80) + ViewThatFits(in: .horizontal) { + HStack { + Spacer() + CircleText( + text: node.user?.shortName ?? "?", + color: Color(UIColor(hex: UInt32(node.num))), + circleSize: 75 + ) + if let user = node.user { + VStack(alignment: .center) { + if user.hwModel != "UNSET" { + Image(user.hardwareImage ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 65, height: 65) + .cornerRadius(5) + Text(String(node.user!.hwModel ?? "unset".localized)) + .font(.caption2) + .frame(maxWidth: 80) + } else { + Image(systemName: "person.crop.circle.badge.questionmark") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 65, height: 65) + .cornerRadius(5) + Text(String("incomplete".localized)) + .font(.caption) + .frame(maxWidth: 80) + } } } - } - - if node.snr != 0 && !node.viaMqtt { - VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.footnote) - Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) - .font(.caption2) - Text("RSSI \(node.rssi)dB") - .foregroundColor(getRssiColor(rssi: node.rssi)) - .font(.caption) + if node.snr != 0 && !node.viaMqtt { + VStack(alignment: .center) { + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") + .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) + .font(.caption2) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption) + } + .frame(minWidth: 110, maxWidth: 175) } - .frame(minWidth: 110, maxWidth: 175) + if node.telemetries?.count ?? 0 > 0 { + BatteryGauge(node: node) + } + Spacer() } - - if node.telemetries?.count ?? 0 > 0 { - BatteryGauge(node: node) - } - Spacer() } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 70762e27..061d45ce 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -186,7 +186,7 @@ struct NodeListItem: View { .frame(width: 30) } if node.hasEnvironmentMetrics { - Image(systemName: "cloud.sun.rain.fill") + Image(systemName: "cloud.sun.rain") .symbolRenderingMode(.hierarchical) .font(.callout) .frame(width: 30) From 7dae25d9498e993e74bd880c935cb105c7d75a0d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 15:59:08 -0700 Subject: [PATCH 035/333] Less parens --- Meshtastic/MeshtasticApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index bea45fa6..4d7dc4c1 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -93,7 +93,7 @@ struct MeshtasticAppleApp: App { if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false - if (((self.incomingUrl?.absoluteString.lowercased().contains("?"))) != nil) { + if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { guard let cs = components.last!.components(separatedBy: "?").first else { return } From 24a4f709fa122798be5f457341d3776d823553e9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 16:18:13 -0700 Subject: [PATCH 036/333] Tighten up the hardware card --- .../Views/Nodes/Helpers/NodeInfoItem.swift | 62 ++++++++++--------- .../Views/Nodes/Helpers/NodeListItem.swift | 1 - 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 0335a6fd..a2ec475d 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -20,54 +20,56 @@ struct NodeInfoItem: View { var body: some View { ViewThatFits(in: .horizontal) { - HStack { - Spacer() - CircleText( - text: node.user?.shortName ?? "?", - color: Color(UIColor(hex: UInt32(node.num))), - circleSize: 75 - ) + VStack { if let user = node.user { - VStack(alignment: .center) { + HStack(alignment: .center) { if user.hwModel != "UNSET" { Image(user.hardwareImage ?? "UNSET") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 65, height: 65) + .frame(width: 75, height: 75) .cornerRadius(5) Text(String(node.user!.hwModel ?? "unset".localized)) - .font(.caption2) - .frame(maxWidth: 80) + .font(.callout) } else { Image(systemName: "person.crop.circle.badge.questionmark") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 65, height: 65) + .frame(width: 75, height: 75) .cornerRadius(5) Text(String("incomplete".localized)) - .font(.caption) - .frame(maxWidth: 80) + .font(.callout) } } } - if node.snr != 0 && !node.viaMqtt { - VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.footnote) - Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) - .font(.caption2) - Text("RSSI \(node.rssi)dB") - .foregroundColor(getRssiColor(rssi: node.rssi)) - .font(.caption) + HStack { + Spacer() + CircleText( + text: node.user?.shortName ?? "?", + color: Color(UIColor(hex: UInt32(node.num))), + circleSize: 75 + ) + if node.snr != 0 && !node.viaMqtt { + VStack(alignment: .center) { + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") + .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) + .font(.caption2) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption) + } + .frame(minWidth: 110, maxWidth: 175) + } else { + Spacer() } - .frame(minWidth: 110, maxWidth: 175) + if node.telemetries?.count ?? 0 > 0 { + BatteryGauge(node: node) + } + Spacer() } - if node.telemetries?.count ?? 0 > 0 { - BatteryGauge(node: node) - } - Spacer() } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 061d45ce..daefa65e 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -21,7 +21,6 @@ struct NodeListItem: View { LazyVStack(alignment: .leading) { HStack { VStack(alignment: .leading) { - CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) .padding(.trailing, 5) BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) From bcb5ecef63ce35f3ea6edc46254695dc8f0bab17 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 16:54:06 -0700 Subject: [PATCH 037/333] Space everything the same --- Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index a2ec475d..5fd83c77 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -27,7 +27,7 @@ struct NodeInfoItem: View { Image(user.hardwareImage ?? "UNSET") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 75, height: 75) + .frame(width: 65, height: 65) .cornerRadius(5) Text(String(node.user!.hwModel ?? "unset".localized)) .font(.callout) @@ -35,14 +35,14 @@ struct NodeInfoItem: View { Image(systemName: "person.crop.circle.badge.questionmark") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 75, height: 75) + .frame(width: 65, height: 65) .cornerRadius(5) Text(String("incomplete".localized)) .font(.callout) } } } - HStack { + HStack(alignment: .center) { Spacer() CircleText( text: node.user?.shortName ?? "?", @@ -50,22 +50,21 @@ struct NodeInfoItem: View { circleSize: 75 ) if node.snr != 0 && !node.viaMqtt { - VStack(alignment: .center) { + Spacer() + VStack { let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) LoRaSignalStrengthIndicator(signalStrength: signalStrength) Text("Signal \(signalStrength.description)").font(.footnote) Text("SNR \(String(format: "%.2f", node.snr))dB") .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) - .font(.caption2) + .font(.caption) Text("RSSI \(node.rssi)dB") .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } - .frame(minWidth: 110, maxWidth: 175) - } else { - Spacer() } if node.telemetries?.count ?? 0 > 0 { + Spacer() BatteryGauge(node: node) } Spacer() From 249b1971803ff9477871bac853056487f95e6ba1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 20:12:43 -0700 Subject: [PATCH 038/333] Remove empty logs header from ios 16 --- Meshtastic/Views/Settings/Settings.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index faae073e..880dabf2 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -243,13 +243,11 @@ struct Settings: View { var loggingSection: some View { Section(header: Text("logging")) { - if #available (iOS 17.0, *) { - NavigationLink(value: SettingsNavigationState.debugLogs) { - Label { - Text("Logs") - } icon: { - Image(systemName: "scroll") - } + NavigationLink(value: SettingsNavigationState.debugLogs) { + Label { + Text("Logs") + } icon: { + Image(systemName: "scroll") } } } @@ -401,7 +399,9 @@ struct Settings: View { radioConfigurationSection deviceConfigurationSection moduleConfigurationSection - loggingSection + if #available (iOS 17.0, *) { + loggingSection + } #if DEBUG developersSection #endif From 440f3b3494c08fa95f28f80c0f84b680858b55c0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 20:31:12 -0700 Subject: [PATCH 039/333] Add device metrics chart for ios 16 --- Meshtastic/Extensions/Int.swift | 4 +- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 51 +++++++++++++++++++ Meshtastic/Views/Nodes/TraceRouteLog.swift | 1 - 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Extensions/Int.swift b/Meshtastic/Extensions/Int.swift index c9087d7f..3f2cb358 100644 --- a/Meshtastic/Extensions/Int.swift +++ b/Meshtastic/Extensions/Int.swift @@ -18,12 +18,12 @@ extension Int { extension UInt32 { func toHex() -> String { - return String(format: "!%2X", self) + return String(format: "!%2X", self).lowercased() } } extension Int64 { func toHex() -> String { - return String(format: "!%2X", self) + return String(format: "!%2X", self).lowercased() } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 3ffce6cb..63e17424 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -108,6 +108,57 @@ struct DeviceMetricsLog: View { .chartLegend(position: .automatic, alignment: .bottom) } else { // Fallback on earlier versions + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + LineMark( + x: .value("x", point.time!), + y: .value("y", point.batteryLevel) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") + .foregroundStyle(batteryChartColor) + .interpolationMethod(.linear) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.channelUtilization) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") + .foregroundStyle(channelUtilizationChartColor) + RuleMark(y: .value("Network Status Orange", 25)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.orange) + RuleMark(y: .value("Network Status Red", 50)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.red) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.airUtilTx) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") + .foregroundStyle(airtimeChartColor) + } + } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartYScale(domain: 0...100) + .chartForegroundStyleScale([ + idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, + "Channel Utilization": channelUtilizationChartColor, + "Airtime": airtimeChartColor + ]) + .chartLegend(position: .automatic, alignment: .bottom) } } .frame(minHeight: 240) diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 9556298a..345a4299 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -63,7 +63,6 @@ struct TraceRouteLog: View { } .font(.title2) } - if selectedRoute?.response ?? false { if selectedRoute?.hasPositions ?? false { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { From d45034cb64bd218ff6207827559d8f8252669fa5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 21:32:47 -0700 Subject: [PATCH 040/333] Update true bearing icons --- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 1 - Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 8 ++++---- Meshtastic/Views/Nodes/NodeList.swift | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 93879e02..9b018b0e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -814,7 +814,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: - if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + if let routingMessage = try? RouteDiscovery(serializedBytes: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true traceRoute?.route = routingMessage.route diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 4bf38ec7..68769280 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -43,7 +43,6 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { HStack { Label { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index daefa65e..ba7dee6e 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -100,8 +100,8 @@ struct NodeListItem: View { .foregroundColor(.gray) let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) let headingDegrees = Angle.degrees(trueBearing) - Image(systemName: "location.north.circle.fill") - .font(.caption) + Image(systemName: "location.north") + .font(.callout) .symbolRenderingMode(.multicolor) .clipShape(Circle()) .rotationEffect(headingDegrees) @@ -126,8 +126,8 @@ struct NodeListItem: View { .foregroundColor(.secondary) let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) let headingDegrees = Angle.degrees(trueBearing) - Image(systemName: "location.north.circle.fill") - .font(.caption) + Image(systemName: "location.north") + .font(.callout) .symbolRenderingMode(.multicolor) .clipShape(Circle()) .rotationEffect(headingDegrees) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 3773593b..10df4299 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -31,7 +31,6 @@ struct NodeList: View { @State private var hopsAway: Double = -1.0 @State private var roleFilter = false @State private var deviceRoles: Set = [] - @State private var isPresentingTraceRouteSentAlert = false @State private var isPresentingPositionSentAlert = false @State private var isPresentingPositionFailedAlert = false From f4bcad2c683a529127d53f839525537684a4c55c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 21:42:02 -0700 Subject: [PATCH 041/333] Linting fixes --- Meshtastic/Extensions/UserDefaults.swift | 2 +- Meshtastic/Helpers/BLEManager.swift | 17 ++++++++--------- .../Views/MapKitMap/Custom/MapViewSwiftUI.swift | 2 +- Meshtastic/Views/Messages/UserMessageList.swift | 2 +- .../Helpers/Actions/DeleteNodeButton.swift | 6 +----- Meshtastic/Views/Nodes/MeshMap.swift | 2 +- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 0fbd680b..ca8441be 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -158,7 +158,7 @@ extension UserDefaults { @UserDefault(.firmwareVersion, defaultValue: "0.0.0") static var firmwareVersion: String - + @UserDefault(.environmentEnableWeatherKit, defaultValue: true) static var environmentEnableWeatherKit: Bool diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9b018b0e..7a683df8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -590,7 +590,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } do { - let logRecord = try LogRecord(serializedData: characteristic.value!) + let logRecord = try LogRecord(serializedBytes: characteristic.value!) var message = logRecord.source.isEmpty ? logRecord.message : "[\(logRecord.source)] \(logRecord.message)" switch logRecord.level { case .debug: @@ -627,7 +627,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var decodedInfo = FromRadio() do { - decodedInfo = try FromRadio(serializedData: characteristic.value!) + decodedInfo = try FromRadio(serializedBytes: characteristic.value!) } catch { Logger.services.error("💥 \(error.localizedDescription, privacy: .public) \(characteristic.value!, privacy: .public)") @@ -870,7 +870,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } case .neighborinfoApp: - if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { + if let neighborInfo = try? NeighborInfo(serializedBytes: decodedInfo.packet.decoded.payload) { // MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED") MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") } @@ -902,7 +902,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(connectedPeripheral.num)) do { - let fetchedNodeInfo = try context.fetch(fetchNodeInfoRequest) ?? [] + let fetchedNodeInfo = try context.fetch(fetchNodeInfoRequest) if fetchedNodeInfo.count == 1 { // Subscribe to Mqtt Client Proxy if enabled if fetchedNodeInfo[0].mqttConfig != nil && fetchedNodeInfo[0].mqttConfig?.enabled ?? false && fetchedNodeInfo[0].mqttConfig?.proxyToClientEnabled ?? false { @@ -1132,7 +1132,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - @MainActor + @MainActor public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { var positionPacket = Position() if #available(iOS 17.0, macOS 14.0, *) { @@ -1265,7 +1265,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum)) Logger.services.debug("📍 \(logString)") return true @@ -1516,14 +1515,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { - let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) + let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData) for cs in channelSet.settings { if addChannels { // We are trying to add a channel so lets get the last index let fetchMyInfoRequest = MyInfoEntity.fetchRequest() fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) ?? [] + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if fetchedMyInfo.count == 1 { i = Int32(fetchedMyInfo[0].channels?.count ?? -1) myInfo = fetchedMyInfo[0] @@ -3001,7 +3000,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { + if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) { // Handle each of the store and forward request / response messages switch storeAndForwardMessage.rr { case .unset: diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index bf196f6d..99c3c766 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -110,7 +110,7 @@ struct MapViewSwiftUI: UIViewRepresentable { overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) - + case .satellite: mapView.mapType = .satellite case .hybrid: diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6514ead8..ce0eb182 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -14,7 +14,7 @@ struct UserMessageList: View { @EnvironmentObject var appState: AppState @EnvironmentObject var bleManager: BLEManager @Environment(\.managedObjectContext) var context - + // Keyboard State @FocusState var messageFieldFocused: Bool // View State Items diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index 71c3a317..ecf16f13 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -3,16 +3,12 @@ import OSLog import SwiftUI struct DeleteNodeButton: View { + var bleManager: BLEManager - var context: NSManagedObjectContext - var connectedNode: NodeInfoEntity - var node: NodeInfoEntity - @Environment(\.dismiss) private var dismiss - @State private var isPresentingAlert = false var body: some View { diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 71cbfaf2..2dc8d105 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -111,7 +111,7 @@ struct MeshMap: View { } .onChange(of: router.navigationState) { guard case .map(let selectedNodeNum) = router.navigationState else { return } - //TODO: handle deep link for waypoints + // TODO: handle deep link for waypoints } .onChange(of: (selectedMapLayer)) { newMapLayer in switch selectedMapLayer { From ce65d07e6593230fc19f5c5a58824f7b2e81e576 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 21:46:20 -0700 Subject: [PATCH 042/333] more lint fixes --- Meshtastic/Persistence/Persistence.swift | 33 ------------------- .../MapKitMap/Custom/MapViewSwiftUI.swift | 1 - 2 files changed, 34 deletions(-) diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 96808c73..93ba7f16 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -103,39 +103,6 @@ extension NSPersistentContainer { case invalidSource(String) } - /// Restore a persistent store for a URL `backupURL`. - /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. - /// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores. - /// - Throws: `CopyPersistentStoreError` in various situations. - /// - Returns: Nothing. If no errors are thrown, the restore is complete. -// func restorePersistentStore(from backupURL: URL) throws -> Void { -// guard backupURL.isFileURL else { -// throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL") -// } -// -// for persistentStoreDescription in persistentStoreDescriptions { -// guard let loadedStoreURL = persistentStoreDescription.url else { -// continue -// } -// guard FileManager.default.fileExists(atPath: backupURL.path) else { -// throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupURL)") -// } -// do { -// let storeOptions = persistentStoreDescription.options -// let configurationName = persistentStoreDescription.configuration -// let storeType = persistentStoreDescription.type -// -// // Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack. -// // When restoring, it's necessary to use the current persistent store coordinator. -// try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupURL, sourceOptions: storeOptions, ofType: storeType) -// // Add the persistent store at the same location we've been using, because it was removed in the previous step. -// try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions) -// } catch { -// throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)") -// } -// } -// } -// /// Restore backup persistent stores located in the directory referenced by `backupURL`. /// /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index 99c3c766..b7629326 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -110,7 +110,6 @@ struct MapViewSwiftUI: UIViewRepresentable { overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) - case .satellite: mapView.mapType = .satellite case .hybrid: From 30f111510a755c805d2738fd9b91da1e7242d524 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 21:57:13 -0700 Subject: [PATCH 043/333] Remove some duplicate logic --- .../Views/Helpers/LoRaSignalStrength.swift | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index 829fd837..c207d084 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -14,36 +14,34 @@ struct LoRaSignalStrengthMeter: View { var compact: Bool var body: some View { - if snr != 0.0 && rssi != 0 { - let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset) - let gradient = Gradient(colors: [.red, .orange, .yellow, .green]) - if !compact { - VStack { - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.footnote) - Text("SNR \(String(format: "%.2f", snr))dB") - .foregroundColor(getSnrColor(snr: snr, preset: ModemPresets.longFast)) - .font(.caption2) - Text("RSSI \(rssi)dB") - .foregroundColor(getRssiColor(rssi: rssi)) - .font(.caption2) - } - } else { - VStack { - Gauge(value: Double(signalStrength.rawValue), in: 0...3) { - } currentValueLabel: { - Image(systemName: "dot.radiowaves.left.and.right") - .font(.callout) - .frame(width: 30) - Text("Signal \(signalStrength.description)") - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - .fixedSize() - } - .gaugeStyle(.accessoryLinear) - .tint(gradient) - .font(.caption) + let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset) + let gradient = Gradient(colors: [.red, .orange, .yellow, .green]) + if !compact { + VStack { + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", snr))dB") + .foregroundColor(getSnrColor(snr: snr, preset: ModemPresets.longFast)) + .font(.caption2) + Text("RSSI \(rssi)dB") + .foregroundColor(getRssiColor(rssi: rssi)) + .font(.caption2) + } + } else { + VStack { + Gauge(value: Double(signalStrength.rawValue), in: 0...3) { + } currentValueLabel: { + Image(systemName: "dot.radiowaves.left.and.right") + .font(.callout) + .frame(width: 30) + Text("Signal \(signalStrength.description)") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) + .fixedSize() } + .gaugeStyle(.accessoryLinear) + .tint(gradient) + .font(.caption) } } } From 03f7d6d37df168cf266a2bc217292efca377a001 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 22:06:31 -0700 Subject: [PATCH 044/333] Revert a couple of lint fixes --- 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 7a683df8..9b798819 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -590,7 +590,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } do { - let logRecord = try LogRecord(serializedBytes: characteristic.value!) + let logRecord = try LogRecord(serializedData: characteristic.value!) var message = logRecord.source.isEmpty ? logRecord.message : "[\(logRecord.source)] \(logRecord.message)" switch logRecord.level { case .debug: @@ -627,7 +627,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var decodedInfo = FromRadio() do { - decodedInfo = try FromRadio(serializedBytes: characteristic.value!) + decodedInfo = try FromRadio(serializedData: characteristic.value!) } catch { Logger.services.error("💥 \(error.localizedDescription, privacy: .public) \(characteristic.value!, privacy: .public)") From 80dba6eccd443a23f032500d5734021af09a74b6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Aug 2024 22:17:49 -0700 Subject: [PATCH 045/333] revert linting changes --- Meshtastic/Helpers/BLEManager.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9b798819..475235de 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -814,7 +814,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: - if let routingMessage = try? RouteDiscovery(serializedBytes: decodedInfo.packet.decoded.payload) { + if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true traceRoute?.route = routingMessage.route @@ -870,7 +870,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } case .neighborinfoApp: - if let neighborInfo = try? NeighborInfo(serializedBytes: decodedInfo.packet.decoded.payload) { + if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { // MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED") MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") } @@ -1515,7 +1515,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { - let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData) + let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) for cs in channelSet.settings { if addChannels { // We are trying to add a channel so lets get the last index @@ -3000,7 +3000,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) { + if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { // Handle each of the store and forward request / response messages switch storeAndForwardMessage.rr { case .unset: From e8b6f00dc2abf8486aab9efaee66c413f74d5e3d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 5 Aug 2024 19:35:41 -0700 Subject: [PATCH 046/333] Device hardware updates --- Meshtastic/Resources/DeviceHardware.json | 64 +++++++++++++------ .../Views/Nodes/Helpers/NodeInfoItem.swift | 31 ++++++++- Meshtastic/Views/Nodes/NodeList.swift | 1 + 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index ad538bd0..16c50d25 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -5,7 +5,7 @@ "platformioTarget": "tlora-v2", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V2" + "displayName": "LILYGO T-LoRa V2" }, { "hwModel": 2, @@ -13,7 +13,7 @@ "platformioTarget": "tlora-v1", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V1" + "displayName": "LILYGO T-LoRa V1" }, { "hwModel": 3, @@ -21,7 +21,7 @@ "platformioTarget": "tlora-v2-1-1_6", "architecture": "esp32", "activelySupported": true, - "displayName": "T-LoRa V2.1-1.6" + "displayName": "LILYGO T-LoRa V2.1-1.6" }, { "hwModel": 4, @@ -29,7 +29,7 @@ "platformioTarget": "tbeam", "architecture": "esp32", "activelySupported": true, - "displayName": "T-Beam" + "displayName": "LILYGO T-Beam" }, { "hwModel": 5, @@ -45,7 +45,7 @@ "platformioTarget": "tbeam0_7", "architecture": "esp32", "activelySupported": false, - "displayName": "T-Beam V0.7" + "displayName": "LILYGO T-Beam V0.7" }, { "hwModel": 7, @@ -53,7 +53,7 @@ "platformioTarget": "t-echo", "architecture": "nrf52840", "activelySupported": true, - "displayName": "T-Echo" + "displayName": "LILYGO T-Echo" }, { "hwModel": 8, @@ -61,7 +61,7 @@ "platformioTarget": "tlora-v1_3", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V1.1-1.3" + "displayName": "LILYGO T-LoRa V1.1-1.3" }, { "hwModel": 9, @@ -69,7 +69,7 @@ "platformioTarget": "rak4631", "architecture": "nrf52840", "activelySupported": true, - "displayName": "RAK4631" + "displayName": "RAK WisBlock 4631" }, { "hwModel": 10, @@ -93,7 +93,7 @@ "platformioTarget": "tbeam-s3-core", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Beam S3 Core" + "displayName": "LILYGO T-Beam S3 Core" }, { "hwModel": 13, @@ -101,7 +101,7 @@ "platformioTarget": "rak11200", "architecture": "esp32", "activelySupported": false, - "displayName": "RAK11200" + "displayName": "RAK WisBlock 11200" }, { "hwModel": 14, @@ -117,7 +117,7 @@ "platformioTarget": "tlora-v2-1-1_8", "architecture": "esp32", "activelySupported": true, - "displayName": "T-LoRa V2.1-1.8" + "displayName": "LILYGO T-LoRa V2.1-1.8" }, { "hwModel": 16, @@ -125,7 +125,7 @@ "platformioTarget": "tlora-t3s3-v1", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-LoRa T3-S3" + "displayName": "LILYGO T-LoRa T3-S3" }, { "hwModel": 17, @@ -143,6 +143,14 @@ "activelySupported": true, "displayName": "Nano G2 Ultra" }, + { + "hwModel": 21, + "hwModelSlug": "WIO_WM1110", + "platformioTarget": "wio-tracker-wm1110", + "architecture": "nrf52840", + "activelySupported": true, + "displayName": "Seeed Wio WM1110 Tracker" + }, { "hwModel": 25, "hwModelSlug": "STATION_G1", @@ -157,7 +165,7 @@ "platformioTarget": "rak11310", "architecture": "rp2040", "activelySupported": true, - "displayName": "RAK11310" + "displayName": "RAK WisBlock 11310" }, { "hwModel": 29, @@ -165,7 +173,7 @@ "platformioTarget": "canaryone", "architecture": "nrf52840", "activelySupported": true, - "displayName": "CanaryOne" + "displayName": "Canary One" }, { "hwModel": 30, @@ -277,7 +285,7 @@ "platformioTarget": "t-deck", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Deck" + "displayName": "LILYGO T-Deck" }, { "hwModel": 51, @@ -285,7 +293,7 @@ "platformioTarget": "t-watch-s3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Watch S3" + "displayName": "LILYGO T-Watch S3" }, { "hwModel": 52, @@ -360,11 +368,27 @@ "displayName": "RadioMaster 900 Bandit Nano" }, { - "hwModel": 21, - "hwModelSlug": "WIO_WM1110", - "platformioTarget": "wio-tracker-wm1110", + "hwModel": 67, + "hwModelSlug": "HELTEC_VISION_MASTER_E213", + "platformioTarget": "heltec-vision-master-e213", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Heltec Vision Master E213" + }, + { + "hwModel": 68, + "hwModelSlug": "HELTEC_VISION_MASTER_E290", + "platformioTarget": "heltec-vision-master-e290", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Heltec Vision Master E290" + }, + { + "hwModel": 71, + "hwModelSlug": "TRACKER_T1000_E", + "platformioTarget": "tracker-t1000-e", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Seeed Wio WM1110 Tracker" + "displayName": "Seeed Card Tracker T1000-E" } ] diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 5fd83c77..34dcdece 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -11,8 +11,9 @@ import MapKit struct NodeInfoItem: View { - @ObservedObject - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity + @State private var currentDevice: DeviceHardware? + @State private var deviceHardware: [DeviceHardware] = [] var modemPreset: ModemPresets = ModemPresets( rawValue: UserDefaults.modemPreset @@ -29,7 +30,7 @@ struct NodeInfoItem: View { .aspectRatio(contentMode: .fit) .frame(width: 65, height: 65) .cornerRadius(5) - Text(String(node.user!.hwModel ?? "unset".localized)) + Text(String(currentDevice?.displayName ?? (node.user?.hwModel ?? "unset".localized))) .font(.callout) } else { Image(systemName: "person.crop.circle.badge.questionmark") @@ -41,6 +42,30 @@ struct NodeInfoItem: View { .font(.callout) } } + .onAppear(perform: { + if currentDevice == nil { + Api().loadDeviceHardwareData { (hw) in + for device in hw { + let currentHardware = node.user?.hwModel ?? "UNSET" + let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") + if deviceString == currentHardware { + currentDevice = device + } + } + } + } + }) + .onChange(of: node) { newNode in + Api().loadDeviceHardwareData { (hw) in + for device in hw { + let currentHardware = newNode.user?.hwModel ?? "UNSET" + let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") + if deviceString == currentHardware { + currentDevice = device + } + } + } + } } HStack(alignment: .center) { Spacer() diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 10df4299..7e9cf865 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -37,6 +37,7 @@ struct NodeList: View { @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 + var boolFilters: [Bool] {[ isOnline, isFavorite, From 204b2ebb321a5fb829cc30026f8a45891b59c0ed Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Aug 2024 09:25:53 -0700 Subject: [PATCH 047/333] Store hardware model id --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/MeshPackets.swift | 1 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 449 ++++++++++++++++++ 4 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0e490c66..cca267e7 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -291,6 +291,7 @@ DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSStatus.swift; sourceTree = ""; }; DD1BD0EA2C601795008C0C70 /* CLLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocation.swift; sourceTree = ""; }; DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; + DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -1828,6 +1829,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */, DD3D17DC2C3D7B1400561584 /* MeshtasticDataModelV 39.xcdatamodel */, @@ -1870,7 +1872,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */; + currentVersion = DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index bb3687b5..7b237b2c 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -287,6 +287,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.longName = nodeInfo.user.longName newUser.shortName = nodeInfo.user.shortName newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) newNode.user = newUser diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 9deb5562..3ddf90f8 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 41.xcdatamodel + MeshtasticDataModelV 42.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents new file mode 100644 index 00000000..bb7621d8 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 894bd6673ceb79dc2017e129a1553d4068b1afe1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Aug 2024 12:57:46 -0700 Subject: [PATCH 048/333] Display name updates --- Meshtastic/Helpers/MeshPackets.swift | 6 ++++ .../contents | 1 + .../Views/Nodes/Helpers/NodeInfoItem.swift | 28 +------------------ 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 7b237b2c..6e259c84 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -288,6 +288,12 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.shortName = nodeInfo.user.shortName newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) + newUser.hwDisplayName = dh?.displayName + } + } newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) newNode.user = newUser diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index bb7621d8..6b2eaa00 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -414,6 +414,7 @@ + diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 34dcdece..69235f11 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -12,8 +12,6 @@ import MapKit struct NodeInfoItem: View { @ObservedObject var node: NodeInfoEntity - @State private var currentDevice: DeviceHardware? - @State private var deviceHardware: [DeviceHardware] = [] var modemPreset: ModemPresets = ModemPresets( rawValue: UserDefaults.modemPreset @@ -30,7 +28,7 @@ struct NodeInfoItem: View { .aspectRatio(contentMode: .fit) .frame(width: 65, height: 65) .cornerRadius(5) - Text(String(currentDevice?.displayName ?? (node.user?.hwModel ?? "unset".localized))) + Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "unset".localized))) .font(.callout) } else { Image(systemName: "person.crop.circle.badge.questionmark") @@ -42,30 +40,6 @@ struct NodeInfoItem: View { .font(.callout) } } - .onAppear(perform: { - if currentDevice == nil { - Api().loadDeviceHardwareData { (hw) in - for device in hw { - let currentHardware = node.user?.hwModel ?? "UNSET" - let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") - if deviceString == currentHardware { - currentDevice = device - } - } - } - } - }) - .onChange(of: node) { newNode in - Api().loadDeviceHardwareData { (hw) in - for device in hw { - let currentHardware = newNode.user?.hwModel ?? "UNSET" - let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") - if deviceString == currentHardware { - currentDevice = device - } - } - } - } } HStack(alignment: .center) { Spacer() From dbdf869c8447905951cdc79f153a57b9ac0dbf21 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Aug 2024 14:53:32 -0700 Subject: [PATCH 049/333] Pretty display name --- Meshtastic/Helpers/MeshPackets.swift | 7 +++++++ Meshtastic/Persistence/UpdateCoreData.swift | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 6e259c84..d2987e46 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -361,6 +361,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) + fetchedNode[0].user!.hwDisplayName = dh?.displayName + } + } } else { if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 3fac0312..507950ce 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -177,6 +177,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.shortName = newUserMessage.shortName newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() + newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) + newUser.hwDisplayName = dh?.displayName + } + } newNode.user = newUser if UserDefaults.newNodeNotifications { @@ -257,6 +264,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) 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) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) + fetchedNode[0].user!.hwDisplayName = dh?.displayName + } + } } } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) From 91022d223eaa84a1e5d0c699b39546c5089a555b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Aug 2024 15:26:16 -0700 Subject: [PATCH 050/333] Crash less --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 507950ce..c407f414 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -267,7 +267,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) Task { Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) fetchedNode[0].user!.hwDisplayName = dh?.displayName } } From 6bb8d03a9807f4d6cd63313786ee5b3ccb1ee96f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Aug 2024 21:56:43 -0700 Subject: [PATCH 051/333] Add display name to search --- Meshtastic/Resources/DeviceHardware.json | 8 ++++++++ Meshtastic/Views/Messages/UserList.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 16c50d25..09eb2e6c 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -367,6 +367,14 @@ "activelySupported": true, "displayName": "RadioMaster 900 Bandit Nano" }, + { + "hwModel": 66, + "hwModelSlug": "HELTEC_VISION_MASTER_T190", + "platformioTarget": "heltec-vision-master-T190", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Heltec Vision Master T190" + }, { "hwModel": 67, "hwModelSlug": "HELTEC_VISION_MASTER_E213", diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 5c7214fe..13eb837d 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -237,7 +237,7 @@ struct UserList: View { private func searchUserList() { /// Case Insensitive Search Text Predicates - let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in + 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 diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 7e9cf865..c442b315 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -344,7 +344,7 @@ struct NodeList: View { private func searchNodeList() async { /// Case Insensitive Search Text Predicates - let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in + let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.hwDisplayName", "user.longName", "user.shortName"].map { property in return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) } /// Create a compound predicate using each text search preicate as an OR From 0ebaeac3ce7126a4d6b63f9052f9c9be70e311c1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 7 Aug 2024 08:06:39 -0700 Subject: [PATCH 052/333] PKI Protos --- .../Sources/meshtastic/admin.pb.swift | 261 +++++++-- .../Sources/meshtastic/apponly.pb.swift | 6 +- .../Sources/meshtastic/atak.pb.swift | 64 ++- .../meshtastic/cannedmessages.pb.swift | 6 +- .../Sources/meshtastic/channel.pb.swift | 37 +- .../Sources/meshtastic/clientonly.pb.swift | 6 +- .../Sources/meshtastic/config.pb.swift | 426 +++++++++------ .../meshtastic/connection_status.pb.swift | 21 +- .../Sources/meshtastic/deviceonly.pb.swift | 33 +- .../Sources/meshtastic/localonly.pb.swift | 9 +- .../Sources/meshtastic/mesh.pb.swift | 509 ++++++++++++++---- .../Sources/meshtastic/module_config.pb.swift | 263 ++++++--- .../Sources/meshtastic/mqtt.pb.swift | 9 +- .../Sources/meshtastic/paxcount.pb.swift | 6 +- .../Sources/meshtastic/portnums.pb.swift | 14 +- .../Sources/meshtastic/powermon.pb.swift | 115 ++-- .../meshtastic/remote_hardware.pb.swift | 35 +- .../Sources/meshtastic/rtttl.pb.swift | 6 +- .../Sources/meshtastic/storeforward.pb.swift | 93 +++- .../Sources/meshtastic/telemetry.pb.swift | 111 ++-- .../Sources/meshtastic/xmodem.pb.swift | 39 +- protobufs | 2 +- 22 files changed, 1489 insertions(+), 582 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 37528079..ba263709 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -24,7 +24,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage: Sendable { +public struct AdminMessage { // 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. @@ -456,7 +456,7 @@ public struct AdminMessage: Sendable { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -590,11 +590,185 @@ public struct AdminMessage: Sendable { /// Tell the node to reset the nodedb. case nodedbReset(Int32) + #if !swift(>=4.1) + public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.getChannelRequest, .getChannelRequest): return { + guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getChannelResponse, .getChannelResponse): return { + guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getOwnerRequest, .getOwnerRequest): return { + guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getOwnerResponse, .getOwnerResponse): return { + guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getConfigRequest, .getConfigRequest): return { + guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getConfigResponse, .getConfigResponse): return { + guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getModuleConfigRequest, .getModuleConfigRequest): return { + guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getModuleConfigResponse, .getModuleConfigResponse): return { + guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { + guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { + guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { + guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { + guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getRingtoneRequest, .getRingtoneRequest): return { + guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getRingtoneResponse, .getRingtoneResponse): return { + guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { + guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { + guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setHamMode, .setHamMode): return { + guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { + guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { + guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.enterDfuModeRequest, .enterDfuModeRequest): return { + guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.deleteFileRequest, .deleteFileRequest): return { + guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setScale, .setScale): return { + guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setOwner, .setOwner): return { + guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setChannel, .setChannel): return { + guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setConfig, .setConfig): return { + guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setModuleConfig, .setModuleConfig): return { + guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { + guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setRingtoneMessage, .setRingtoneMessage): return { + guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeByNodenum, .removeByNodenum): return { + guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFavoriteNode, .setFavoriteNode): return { + guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFavoriteNode, .removeFavoriteNode): return { + guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFixedPosition, .setFixedPosition): return { + guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFixedPosition, .removeFixedPosition): return { + guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.beginEditSettings, .beginEditSettings): return { + guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.commitEditSettings, .commitEditSettings): return { + guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebootOtaSeconds, .rebootOtaSeconds): return { + guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.exitSimulator, .exitSimulator): return { + guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebootSeconds, .rebootSeconds): return { + guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.shutdownSeconds, .shutdownSeconds): return { + guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.factoryReset, .factoryReset): return { + guard case .factoryReset(let l) = lhs, case .factoryReset(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.nodedbReset, .nodedbReset): return { + guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ConfigType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -656,22 +830,11 @@ public struct AdminMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - ] - } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ModuleConfigType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -769,31 +932,50 @@ public struct AdminMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] - } public init() {} } +#if swift(>=4.2) + +extension AdminMessage.ConfigType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + ] +} + +extension AdminMessage.ModuleConfigType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] +} + +#endif // swift(>=4.2) + /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters: Sendable { +public struct HamParameters { // 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. @@ -823,7 +1005,7 @@ public struct HamParameters: Sendable { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse: Sendable { +public struct NodeRemoteHardwarePinsResponse { // 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. @@ -837,6 +1019,15 @@ public struct NodeRemoteHardwarePinsResponse: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension AdminMessage: @unchecked Sendable {} +extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} +extension AdminMessage.ConfigType: @unchecked Sendable {} +extension AdminMessage.ModuleConfigType: @unchecked Sendable {} +extension HamParameters: @unchecked Sendable {} +extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1534,7 +1725,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency.bitPattern != 0 { + if self.frequency != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 18e66d8e..0457077c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet: Sendable { +public struct ChannelSet { // 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. @@ -53,6 +53,10 @@ public struct ChannelSet: Sendable { fileprivate var _loraConfig: Config.LoRaConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension ChannelSet: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 1dd12469..4406deb3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum Team: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -130,6 +130,11 @@ public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -148,12 +153,13 @@ public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { .darkGreen, .brown, ] - } +#endif // swift(>=4.2) + /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum MemberRole: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -227,6 +233,11 @@ public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -239,12 +250,13 @@ public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { .rto, .k9, ] - } +#endif // swift(>=4.2) + /// /// Packets for the official ATAK Plugin -public struct TAKPacket: Sendable { +public struct TAKPacket { // 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. @@ -314,7 +326,7 @@ public struct TAKPacket: Sendable { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TAK position report case pli(PLI) @@ -322,6 +334,24 @@ public struct TAKPacket: Sendable { /// ATAK GeoChat message case chat(GeoChat) + #if !swift(>=4.1) + public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.pli, .pli): return { + guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.chat, .chat): return { + guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -333,7 +363,7 @@ public struct TAKPacket: Sendable { /// /// ATAK GeoChat message -public struct GeoChat: Sendable { +public struct GeoChat { // 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. @@ -375,7 +405,7 @@ public struct GeoChat: Sendable { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group: Sendable { +public struct Group { // 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. @@ -397,7 +427,7 @@ public struct Group: Sendable { /// /// ATAK EUD Status /// -public struct Status: Sendable { +public struct Status { // 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. @@ -414,7 +444,7 @@ public struct Status: Sendable { /// /// ATAK Contact /// -public struct Contact: Sendable { +public struct Contact { // 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. @@ -434,7 +464,7 @@ public struct Contact: Sendable { /// /// Position Location Information from ATAK -public struct PLI: Sendable { +public struct PLI { // 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. @@ -466,6 +496,18 @@ public struct PLI: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Team: @unchecked Sendable {} +extension MemberRole: @unchecked Sendable {} +extension TAKPacket: @unchecked Sendable {} +extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} +extension GeoChat: @unchecked Sendable {} +extension Group: @unchecked Sendable {} +extension Status: @unchecked Sendable {} +extension Contact: @unchecked Sendable {} +extension PLI: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index a43393e1..1b8c84de 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig: Sendable { +public struct CannedMessageModuleConfig { // 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. @@ -36,6 +36,10 @@ public struct CannedMessageModuleConfig: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension CannedMessageModuleConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index a8c96595..5b9c7e49 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -36,15 +36,13 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings: @unchecked Sendable { +public struct ChannelSettings { // 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. /// /// Deprecated in favor of LoraConfig.channel_num - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -113,7 +111,7 @@ public struct ChannelSettings: @unchecked Sendable { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings: Sendable { +public struct ModuleSettings { // 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. @@ -134,7 +132,7 @@ public struct ModuleSettings: Sendable { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel: Sendable { +public struct Channel { // 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. @@ -172,7 +170,7 @@ public struct Channel: Sendable { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Role: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -211,13 +209,6 @@ public struct Channel: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] - } public init() {} @@ -225,6 +216,26 @@ public struct Channel: Sendable { fileprivate var _settings: ChannelSettings? = nil } +#if swift(>=4.2) + +extension Channel.Role: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension ChannelSettings: @unchecked Sendable {} +extension ModuleSettings: @unchecked Sendable {} +extension Channel: @unchecked Sendable {} +extension Channel.Role: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index 89370cc5..c3d93bf7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile: Sendable { +public struct DeviceProfile { // 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. @@ -94,6 +94,10 @@ public struct DeviceProfile: Sendable { fileprivate var _moduleConfig: LocalModuleConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension DeviceProfile: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index aeaf9054..f396367c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config: Sendable { +public struct Config { // 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. @@ -89,7 +89,7 @@ public struct Config: Sendable { /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -98,11 +98,49 @@ public struct Config: Sendable { case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) + #if !swift(>=4.1) + public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.device, .device): return { + guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.position, .position): return { + guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.power, .power): return { + guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.network, .network): return { + guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.display, .display): return { + guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.lora, .lora): return { + guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.bluetooth, .bluetooth): return { + guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// Configuration - public struct DeviceConfig: Sendable { + public struct DeviceConfig { // 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. @@ -164,7 +202,7 @@ public struct Config: Sendable { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Role: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -182,8 +220,6 @@ public struct Config: Sendable { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 - - /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -274,26 +310,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - ] - } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RebroadcastMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -341,14 +362,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - ] - } public init() {} @@ -356,7 +369,7 @@ public struct Config: Sendable { /// /// Position Config - public struct PositionConfig: Sendable { + public struct PositionConfig { // 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. @@ -378,8 +391,6 @@ public struct Config: Sendable { /// /// Is GPS enabled for this node? - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -390,8 +401,6 @@ public struct Config: Sendable { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -432,7 +441,7 @@ public struct Config: Sendable { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum PositionFlags: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -522,24 +531,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] - } - public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum GpsMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -577,13 +571,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] - } public init() {} @@ -592,7 +579,7 @@ public struct Config: Sendable { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig: Sendable { + public struct PowerConfig { // 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. @@ -652,7 +639,7 @@ public struct Config: Sendable { /// /// Network Config - public struct NetworkConfig: Sendable { + public struct NetworkConfig { // 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. @@ -699,7 +686,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum AddressMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -731,15 +718,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] - } - public struct IpV4Config: Sendable { + public struct IpV4Config { // 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. @@ -772,7 +753,7 @@ public struct Config: Sendable { /// /// Display Config - public struct DisplayConfig: Sendable { + public struct DisplayConfig { // 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. @@ -828,7 +809,7 @@ public struct Config: Sendable { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -891,21 +872,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] - } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum DisplayUnits: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -937,17 +908,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] - } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum OledType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -991,17 +956,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - ] - } - public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum DisplayMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1045,17 +1002,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] - } - public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum CompassOrientation: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1123,18 +1072,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] - } public init() {} @@ -1142,7 +1079,7 @@ public struct Config: Sendable { /// /// Lora Config - public struct LoRaConfig: @unchecked Sendable { + public struct LoRaConfig { // 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. @@ -1299,7 +1236,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RegionCode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1433,35 +1370,12 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - ] - } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ModemPreset: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1529,18 +1443,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - ] - } public init() {} @@ -1548,7 +1450,7 @@ public struct Config: Sendable { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig: Sendable { + public struct BluetoothConfig { // 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. @@ -1571,7 +1473,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum PairingMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1609,13 +1511,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] - } public init() {} @@ -1624,6 +1519,199 @@ public struct Config: Sendable { public init() {} } +#if swift(>=4.2) + +extension Config.DeviceConfig.Role: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + ] +} + +extension Config.DeviceConfig.RebroadcastMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + ] +} + +extension Config.PositionConfig.PositionFlags: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] +} + +extension Config.PositionConfig.GpsMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] +} + +extension Config.NetworkConfig.AddressMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] +} + +extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] +} + +extension Config.DisplayConfig.DisplayUnits: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] +} + +extension Config.DisplayConfig.OledType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + ] +} + +extension Config.DisplayConfig.DisplayMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] +} + +extension Config.DisplayConfig.CompassOrientation: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] +} + +extension Config.LoRaConfig.RegionCode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + ] +} + +extension Config.LoRaConfig.ModemPreset: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + ] +} + +extension Config.BluetoothConfig.PairingMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension Config: @unchecked Sendable {} +extension Config.OneOf_PayloadVariant: @unchecked Sendable {} +extension Config.DeviceConfig: @unchecked Sendable {} +extension Config.DeviceConfig.Role: @unchecked Sendable {} +extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} +extension Config.PositionConfig: @unchecked Sendable {} +extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} +extension Config.PositionConfig.GpsMode: @unchecked Sendable {} +extension Config.PowerConfig: @unchecked Sendable {} +extension Config.NetworkConfig: @unchecked Sendable {} +extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} +extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} +extension Config.DisplayConfig: @unchecked Sendable {} +extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} +extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} +extension Config.DisplayConfig.OledType: @unchecked Sendable {} +extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} +extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} +extension Config.LoRaConfig: @unchecked Sendable {} +extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} +extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} +extension Config.BluetoothConfig: @unchecked Sendable {} +extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2080,7 +2168,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride.bitPattern != 0 { + if self.adcMultiplierOverride != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2524,7 +2612,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset.bitPattern != 0 { + if _storage._frequencyOffset != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2548,7 +2636,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency.bitPattern != 0 { + if _storage._overrideFrequency != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a4569714..a2ec180e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus: Sendable { +public struct DeviceConnectionStatus { // 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus: Sendable { /// /// WiFi connection status -public struct WifiConnectionStatus: Sendable { +public struct WifiConnectionStatus { // 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus: Sendable { /// /// Ethernet connection status -public struct EthernetConnectionStatus: Sendable { +public struct EthernetConnectionStatus { // 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus: Sendable { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus: Sendable { +public struct NetworkConnectionStatus { // 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus: Sendable { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus: Sendable { +public struct BluetoothConnectionStatus { // 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus: Sendable { /// /// Serial connection status -public struct SerialConnectionStatus: Sendable { +public struct SerialConnectionStatus { // 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. @@ -209,6 +209,15 @@ public struct SerialConnectionStatus: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension DeviceConnectionStatus: @unchecked Sendable {} +extension WifiConnectionStatus: @unchecked Sendable {} +extension EthernetConnectionStatus: @unchecked Sendable {} +extension NetworkConnectionStatus: @unchecked Sendable {} +extension BluetoothConnectionStatus: @unchecked Sendable {} +extension SerialConnectionStatus: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 834f9636..10b9af2b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum ScreenFonts: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -60,18 +60,24 @@ public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, ] - } +#endif // swift(>=4.2) + /// /// Position with static location information only for NodeDBLite -public struct PositionLite: Sendable { +public struct PositionLite { // 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. @@ -106,7 +112,7 @@ public struct PositionLite: Sendable { public init() {} } -public struct NodeInfoLite: @unchecked Sendable { +public struct NodeInfoLite { // 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. @@ -209,7 +215,7 @@ public struct NodeInfoLite: @unchecked Sendable { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState: @unchecked Sendable { +public struct DeviceState { // 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. @@ -269,8 +275,6 @@ public struct DeviceState: @unchecked Sendable { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -319,7 +323,7 @@ public struct DeviceState: @unchecked Sendable { /// /// The on-disk saved channels -public struct ChannelFile: Sendable { +public struct ChannelFile { // 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. @@ -342,7 +346,7 @@ public struct ChannelFile: Sendable { /// /// This can be used for customizing the firmware distribution. If populated, /// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore: @unchecked Sendable { +public struct OEMStore { // 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. @@ -401,6 +405,15 @@ public struct OEMStore: @unchecked Sendable { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension ScreenFonts: @unchecked Sendable {} +extension PositionLite: @unchecked Sendable {} +extension NodeInfoLite: @unchecked Sendable {} +extension DeviceState: @unchecked Sendable {} +extension ChannelFile: @unchecked Sendable {} +extension OEMStore: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -568,7 +581,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr.bitPattern != 0 { + if _storage._snr != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 17dd5baa..5e30d1cd 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig: @unchecked Sendable { +public struct LocalConfig { // 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. @@ -118,7 +118,7 @@ public struct LocalConfig: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig: @unchecked Sendable { +public struct LocalModuleConfig { // 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. @@ -282,6 +282,11 @@ public struct LocalModuleConfig: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=5.5) && canImport(_Concurrency) +extension LocalConfig: @unchecked Sendable {} +extension LocalModuleConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 996e8268..b12cc13f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -25,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum HardwareModel: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -496,6 +496,11 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -571,12 +576,13 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .trackerT1000E, .privateHw, ] - } +#endif // swift(>=4.2) + /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum Constants: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -611,20 +617,26 @@ public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] - } +#endif // swift(>=4.2) + /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum CriticalErrorCode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -676,6 +688,17 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { /// A (likely software but possibly hardware) failure was detected while trying to send packets. /// If this occurs on your board, please post in the forum so that we can ask you to collect some information to allow fixing this bug case radioSpiBug // = 11 + + /// + /// Corruption was detected on the flash filesystem but we were able to repair things. + /// If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. + case flashCorruptionRecoverable // = 12 + + /// + /// Corruption was detected on the flash filesystem but we were unable to repair things. + /// NOTE: Your node will probably need to be reconfigured the next time it reboots (it will lose the region code etc...) + /// If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. + case flashCorruptionUnrecoverable // = 13 case UNRECOGNIZED(Int) public init() { @@ -696,6 +719,8 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { case 9: self = .brownout case 10: self = .sx1262Failure case 11: self = .radioSpiBug + case 12: self = .flashCorruptionRecoverable + case 13: self = .flashCorruptionUnrecoverable default: self = .UNRECOGNIZED(rawValue) } } @@ -714,10 +739,17 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { case .brownout: return 9 case .sx1262Failure: return 10 case .radioSpiBug: return 11 + case .flashCorruptionRecoverable: return 12 + case .flashCorruptionUnrecoverable: return 13 case .UNRECOGNIZED(let i): return i } } +} + +#if swift(>=4.2) + +extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -732,13 +764,16 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { .brownout, .sx1262Failure, .radioSpiBug, + .flashCorruptionRecoverable, + .flashCorruptionUnrecoverable, ] - } +#endif // swift(>=4.2) + /// /// a gps position -public struct Position: @unchecked Sendable { +public struct Position { // 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. @@ -927,7 +962,7 @@ public struct Position: @unchecked Sendable { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum LocSource: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -971,20 +1006,12 @@ public struct Position: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] - } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum AltSource: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1034,15 +1061,6 @@ public struct Position: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] - } public init() {} @@ -1050,6 +1068,31 @@ public struct Position: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=4.2) + +extension Position.LocSource: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] +} + +extension Position.AltSource: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] +} + +#endif // swift(>=4.2) + /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1071,7 +1114,7 @@ public struct Position: @unchecked Sendable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User: @unchecked Sendable { +public struct User { // 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. @@ -1096,8 +1139,6 @@ public struct User: @unchecked Sendable { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1124,7 +1165,7 @@ public struct User: @unchecked Sendable { /// /// A message used in our Dynamic Source Routing protocol (RFC 4728 based) -public struct RouteDiscovery: Sendable { +public struct RouteDiscovery { // 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. @@ -1140,7 +1181,7 @@ public struct RouteDiscovery: Sendable { /// /// A Routing control Data packet handled by the routing module -public struct Routing: Sendable { +public struct Routing { // 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. @@ -1180,7 +1221,7 @@ public struct Routing: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable, Sendable { + public enum OneOf_Variant: Equatable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1192,12 +1233,34 @@ public struct Routing: Sendable { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) + #if !swift(>=4.1) + public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.routeRequest, .routeRequest): return { + guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.routeReply, .routeReply): return { + guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorReason, .errorReason): return { + guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Error: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1291,32 +1354,38 @@ public struct Routing: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - ] - } public init() {} } +#if swift(>=4.2) + +extension Routing.Error: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + ] +} + +#endif // swift(>=4.2) + /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage: @unchecked Sendable { +public struct DataMessage { // 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. @@ -1370,7 +1439,7 @@ public struct DataMessage: @unchecked Sendable { /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint: Sendable { +public struct Waypoint { // 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. @@ -1415,7 +1484,7 @@ public struct Waypoint: Sendable { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage: @unchecked Sendable { +public struct MqttClientProxyMessage { // 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. @@ -1456,7 +1525,7 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Bytes case data(Data) @@ -1464,6 +1533,24 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// Text case text(String) + #if !swift(>=4.1) + public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.data, .data): return { + guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -1473,7 +1560,7 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket: @unchecked Sendable { +public struct MeshPacket { // 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. @@ -1607,8 +1694,6 @@ public struct MeshPacket: @unchecked Sendable { /// /// Describe if this message is delayed - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -1631,7 +1716,7 @@ public struct MeshPacket: @unchecked Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1639,6 +1724,24 @@ public struct MeshPacket: @unchecked Sendable { /// TODO: REPLACE case encrypted(Data) + #if !swift(>=4.1) + public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.decoded, .decoded): return { + guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.encrypted, .encrypted): return { + guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// @@ -1660,7 +1763,7 @@ public struct MeshPacket: @unchecked Sendable { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Priority: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1725,22 +1828,11 @@ public struct MeshPacket: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .ack, - .max, - ] - } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Delayed: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1778,13 +1870,6 @@ public struct MeshPacket: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] - } public init() {} @@ -1792,6 +1877,32 @@ public struct MeshPacket: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=4.2) + +extension MeshPacket.Priority: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .ack, + .max, + ] +} + +extension MeshPacket.Delayed: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] +} + +#endif // swift(>=4.2) + /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -1809,7 +1920,7 @@ public struct MeshPacket: @unchecked Sendable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo: @unchecked Sendable { +public struct NodeInfo { // 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. @@ -1910,7 +2021,7 @@ public struct NodeInfo: @unchecked Sendable { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo: Sendable { +public struct MyNodeInfo { // 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. @@ -1941,7 +2052,7 @@ public struct MyNodeInfo: Sendable { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord: Sendable { +public struct LogRecord { // 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. @@ -1966,7 +2077,7 @@ public struct LogRecord: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Level: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -2028,23 +2139,29 @@ public struct LogRecord: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] - } public init() {} } -public struct QueueStatus: Sendable { +#if swift(>=4.2) + +extension LogRecord.Level: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] +} + +#endif // swift(>=4.2) + +public struct QueueStatus { // 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. @@ -2071,7 +2188,7 @@ public struct QueueStatus: Sendable { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio: Sendable { +public struct FromRadio { // 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. @@ -2237,7 +2354,7 @@ public struct FromRadio: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2289,6 +2406,72 @@ public struct FromRadio: Sendable { /// File system manifest messages case fileInfo(FileInfo) + #if !swift(>=4.1) + public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.packet, .packet): return { + guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.myInfo, .myInfo): return { + guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.nodeInfo, .nodeInfo): return { + guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.config, .config): return { + guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.logRecord, .logRecord): return { + guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.configCompleteID, .configCompleteID): return { + guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebooted, .rebooted): return { + guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.moduleConfig, .moduleConfig): return { + guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.channel, .channel): return { + guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.queueStatus, .queueStatus): return { + guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.metadata, .metadata): return { + guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { + guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileInfo, .fileInfo): return { + guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -2296,7 +2479,7 @@ public struct FromRadio: Sendable { /// /// Individual File info for the device -public struct FileInfo: Sendable { +public struct FileInfo { // 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. @@ -2317,7 +2500,7 @@ public struct FileInfo: Sendable { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio: Sendable { +public struct ToRadio { // 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. @@ -2397,7 +2580,7 @@ public struct ToRadio: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -2424,6 +2607,40 @@ public struct ToRadio: Sendable { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) + #if !swift(>=4.1) + public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.packet, .packet): return { + guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.wantConfigID, .wantConfigID): return { + guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.disconnect, .disconnect): return { + guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { + guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -2431,7 +2648,7 @@ public struct ToRadio: Sendable { /// /// Compressed message payload -public struct Compressed: @unchecked Sendable { +public struct Compressed { // 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. @@ -2451,7 +2668,7 @@ public struct Compressed: @unchecked Sendable { /// /// Full info on edges for a single node -public struct NeighborInfo: Sendable { +public struct NeighborInfo { // 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. @@ -2479,7 +2696,7 @@ public struct NeighborInfo: Sendable { /// /// A single edge in the mesh -public struct Neighbor: Sendable { +public struct Neighbor { // 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. @@ -2509,7 +2726,7 @@ public struct Neighbor: Sendable { /// /// Device metadata response -public struct DeviceMetadata: Sendable { +public struct DeviceMetadata { // 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. @@ -2562,7 +2779,7 @@ public struct DeviceMetadata: Sendable { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat: Sendable { +public struct Heartbeat { // 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. @@ -2574,7 +2791,7 @@ public struct Heartbeat: Sendable { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin: Sendable { +public struct NodeRemoteHardwarePin { // 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. @@ -2601,7 +2818,7 @@ public struct NodeRemoteHardwarePin: Sendable { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload: @unchecked Sendable { +public struct ChunkedPayload { // 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. @@ -2629,7 +2846,7 @@ public struct ChunkedPayload: @unchecked Sendable { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks: Sendable { +public struct resend_chunks { // 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. @@ -2643,7 +2860,7 @@ public struct resend_chunks: Sendable { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse: Sendable { +public struct ChunkedPayloadResponse { // 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. @@ -2686,7 +2903,7 @@ public struct ChunkedPayloadResponse: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -2697,11 +2914,75 @@ public struct ChunkedPayloadResponse: Sendable { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) + #if !swift(>=4.1) + public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.requestTransfer, .requestTransfer): return { + guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.acceptTransfer, .acceptTransfer): return { + guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.resendChunks, .resendChunks): return { + guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension HardwareModel: @unchecked Sendable {} +extension Constants: @unchecked Sendable {} +extension CriticalErrorCode: @unchecked Sendable {} +extension Position: @unchecked Sendable {} +extension Position.LocSource: @unchecked Sendable {} +extension Position.AltSource: @unchecked Sendable {} +extension User: @unchecked Sendable {} +extension RouteDiscovery: @unchecked Sendable {} +extension Routing: @unchecked Sendable {} +extension Routing.OneOf_Variant: @unchecked Sendable {} +extension Routing.Error: @unchecked Sendable {} +extension DataMessage: @unchecked Sendable {} +extension Waypoint: @unchecked Sendable {} +extension MqttClientProxyMessage: @unchecked Sendable {} +extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} +extension MeshPacket: @unchecked Sendable {} +extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} +extension MeshPacket.Priority: @unchecked Sendable {} +extension MeshPacket.Delayed: @unchecked Sendable {} +extension NodeInfo: @unchecked Sendable {} +extension MyNodeInfo: @unchecked Sendable {} +extension LogRecord: @unchecked Sendable {} +extension LogRecord.Level: @unchecked Sendable {} +extension QueueStatus: @unchecked Sendable {} +extension FromRadio: @unchecked Sendable {} +extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension FileInfo: @unchecked Sendable {} +extension ToRadio: @unchecked Sendable {} +extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension Compressed: @unchecked Sendable {} +extension NeighborInfo: @unchecked Sendable {} +extension Neighbor: @unchecked Sendable {} +extension DeviceMetadata: @unchecked Sendable {} +extension Heartbeat: @unchecked Sendable {} +extension NodeRemoteHardwarePin: @unchecked Sendable {} +extension ChunkedPayload: @unchecked Sendable {} +extension resend_chunks: @unchecked Sendable {} +extension ChunkedPayloadResponse: @unchecked Sendable {} +extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2804,6 +3085,8 @@ extension CriticalErrorCode: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "BROWNOUT"), 10: .same(proto: "SX1262_FAILURE"), 11: .same(proto: "RADIO_SPI_BUG"), + 12: .same(proto: "FLASH_CORRUPTION_RECOVERABLE"), + 13: .same(proto: "FLASH_CORRUPTION_UNRECOVERABLE"), ] } @@ -3647,7 +3930,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr.bitPattern != 0 { + if _storage._rxSnr != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -3822,7 +4105,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr.bitPattern != 0 { + if _storage._snr != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -4595,7 +4878,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr.bitPattern != 0 { + if self.snr != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -4708,8 +4991,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 6f3b2d76..c68ffd83 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -58,18 +58,24 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] - } +#endif // swift(>=4.2) + /// /// Module Config -public struct ModuleConfig: Sendable { +public struct ModuleConfig { // 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. @@ -212,7 +218,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -253,11 +259,73 @@ public struct ModuleConfig: Sendable { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) + #if !swift(>=4.1) + public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.mqtt, .mqtt): return { + guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.serial, .serial): return { + guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.externalNotification, .externalNotification): return { + guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.storeForward, .storeForward): return { + guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rangeTest, .rangeTest): return { + guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.telemetry, .telemetry): return { + guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.cannedMessage, .cannedMessage): return { + guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.audio, .audio): return { + guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.remoteHardware, .remoteHardware): return { + guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.neighborInfo, .neighborInfo): return { + guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.ambientLighting, .ambientLighting): return { + guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.detectionSensor, .detectionSensor): return { + guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.paxcounter, .paxcounter): return { + guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// MQTT Client Config - public struct MQTTConfig: Sendable { + public struct MQTTConfig { // 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. @@ -332,7 +400,7 @@ public struct ModuleConfig: Sendable { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings: Sendable { + public struct MapReportSettings { // 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. @@ -352,7 +420,7 @@ public struct ModuleConfig: Sendable { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig: Sendable { + public struct RemoteHardwareConfig { // 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. @@ -376,7 +444,7 @@ public struct ModuleConfig: Sendable { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig: Sendable { + public struct NeighborInfoConfig { // 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. @@ -397,7 +465,7 @@ public struct ModuleConfig: Sendable { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig: Sendable { + public struct DetectionSensorConfig { // 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. @@ -448,7 +516,7 @@ public struct ModuleConfig: Sendable { /// /// Audio Config for codec2 voice - public struct AudioConfig: Sendable { + public struct AudioConfig { // 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. @@ -485,7 +553,7 @@ public struct ModuleConfig: Sendable { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Audio_Baud: SwiftProtobuf.Enum { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -532,19 +600,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] - } public init() {} @@ -552,7 +607,7 @@ public struct ModuleConfig: Sendable { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig: Sendable { + public struct PaxcounterConfig { // 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. @@ -578,7 +633,7 @@ public struct ModuleConfig: Sendable { /// /// Serial Config - public struct SerialConfig: Sendable { + public struct SerialConfig { // 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. @@ -621,7 +676,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Serial_Baud: SwiftProtobuf.Enum { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -689,31 +744,11 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] - } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Serial_Mode: SwiftProtobuf.Enum { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -758,17 +793,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] - } public init() {} @@ -776,7 +800,7 @@ public struct ModuleConfig: Sendable { /// /// External Notifications Config - public struct ExternalNotificationConfig: Sendable { + public struct ExternalNotificationConfig { // 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. @@ -859,7 +883,7 @@ public struct ModuleConfig: Sendable { /// /// Store and Forward Module Config - public struct StoreForwardConfig: Sendable { + public struct StoreForwardConfig { // 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. @@ -895,7 +919,7 @@ public struct ModuleConfig: Sendable { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig: Sendable { + public struct RangeTestConfig { // 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. @@ -920,7 +944,7 @@ public struct ModuleConfig: Sendable { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig: Sendable { + public struct TelemetryConfig { // 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. @@ -977,7 +1001,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public struct CannedMessageConfig: Sendable { + public struct CannedMessageConfig { // 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. @@ -1032,7 +1056,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum InputEventChar: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1100,18 +1124,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] - } public init() {} @@ -1120,7 +1132,7 @@ public struct ModuleConfig: Sendable { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig: Sendable { + public struct AmbientLightingConfig { // 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. @@ -1153,9 +1165,77 @@ public struct ModuleConfig: Sendable { public init() {} } +#if swift(>=4.2) + +extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] +} + +extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] +} + +extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + ] +} + +extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] +} + +#endif // swift(>=4.2) + /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin: Sendable { +public struct RemoteHardwarePin { // 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. @@ -1177,6 +1257,31 @@ public struct RemoteHardwarePin: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension RemoteHardwarePinType: @unchecked Sendable {} +extension ModuleConfig: @unchecked Sendable {} +extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} +extension ModuleConfig.MQTTConfig: @unchecked Sendable {} +extension ModuleConfig.MapReportSettings: @unchecked Sendable {} +extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} +extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} +extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} +extension ModuleConfig.AudioConfig: @unchecked Sendable {} +extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} +extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} +extension ModuleConfig.SerialConfig: @unchecked Sendable {} +extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} +extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} +extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} +extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} +extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} +extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} +extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} +extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} +extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} +extension RemoteHardwarePin: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index fc5e37a1..efe6cdd5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope: Sendable { +public struct ServiceEnvelope { // 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope: Sendable { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport: Sendable { +public struct MapReport { // 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. @@ -121,6 +121,11 @@ public struct MapReport: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension ServiceEnvelope: @unchecked Sendable {} +extension MapReport: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index f82b3c51..cf8aa463 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount: Sendable { +public struct Paxcount { // 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. @@ -44,6 +44,10 @@ public struct Paxcount: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Paxcount: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index c5348a8a..c728c961 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum PortNum: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -277,6 +277,11 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -308,9 +313,14 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { .atakForwarder, .max, ] - } +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PortNum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 9c61e6d0..5f51e948 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon: Sendable { +public struct PowerMon { // 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. @@ -31,7 +31,7 @@ public struct PowerMon: Sendable { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum State: SwiftProtobuf.Enum { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,31 +104,37 @@ public struct PowerMon: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] - } public init() {} } +#if swift(>=4.2) + +extension PowerMon.State: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] +} + +#endif // swift(>=4.2) + /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage: Sendable { +public struct PowerStressMessage { // 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. @@ -145,7 +151,7 @@ public struct PowerStressMessage: Sendable { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Opcode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -266,35 +272,48 @@ public struct PowerStressMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] - } public init() {} } +#if swift(>=4.2) + +extension PowerStressMessage.Opcode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PowerMon: @unchecked Sendable {} +extension PowerMon.State: @unchecked Sendable {} +extension PowerStressMessage: @unchecked Sendable {} +extension PowerStressMessage.Opcode: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -304,8 +323,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } public func traverse(visitor: inout V) throws { @@ -360,7 +379,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds.bitPattern != 0 { + if self.numSeconds != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index 60f64504..ac6eeb26 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage: Sendable { +public struct HardwareMessage { // 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. @@ -52,7 +52,7 @@ public struct HardwareMessage: Sendable { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum TypeEnum: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -110,21 +110,32 @@ public struct HardwareMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] - } public init() {} } +#if swift(>=4.2) + +extension HardwareMessage.TypeEnum: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension HardwareMessage: @unchecked Sendable {} +extension HardwareMessage.TypeEnum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index c1f3f678..6fdf3208 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig: Sendable { +public struct RTTTLConfig { // 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. @@ -36,6 +36,10 @@ public struct RTTTLConfig: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension RTTTLConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 0b67eaf6..54efa77b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward: @unchecked Sendable { +public struct StoreAndForward { // 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. @@ -79,7 +79,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable, @unchecked Sendable { + public enum OneOf_Variant: Equatable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,12 +93,38 @@ public struct StoreAndForward: @unchecked Sendable { /// Text from history message. case text(Data) + #if !swift(>=4.1) + public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.stats, .stats): return { + guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.history, .history): return { + guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RequestResponse: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -216,31 +242,11 @@ public struct StoreAndForward: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] - } /// /// TODO: REPLACE - public struct Statistics: Sendable { + public struct Statistics { // 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. @@ -288,7 +294,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public struct History: Sendable { + public struct History { // 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. @@ -313,7 +319,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public struct Heartbeat: Sendable { + public struct Heartbeat { // 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. @@ -334,6 +340,41 @@ public struct StoreAndForward: @unchecked Sendable { public init() {} } +#if swift(>=4.2) + +extension StoreAndForward.RequestResponse: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension StoreAndForward: @unchecked Sendable {} +extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} +extension StoreAndForward.RequestResponse: @unchecked Sendable {} +extension StoreAndForward.Statistics: @unchecked Sendable {} +extension StoreAndForward.History: @unchecked Sendable {} +extension StoreAndForward.Heartbeat: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 7a9b81de..ec627e3d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum TelemetrySensorType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -198,6 +198,11 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -227,12 +232,13 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .dfrobotLark, .nau7802, ] - } +#endif // swift(>=4.2) + /// /// Key native device metrics such as battery level -public struct DeviceMetrics: Sendable { +public struct DeviceMetrics { // 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. @@ -264,7 +270,7 @@ public struct DeviceMetrics: Sendable { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics: @unchecked Sendable { +public struct EnvironmentMetrics { // 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. @@ -399,7 +405,7 @@ public struct EnvironmentMetrics: @unchecked Sendable { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics: Sendable { +public struct PowerMetrics { // 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. @@ -435,7 +441,7 @@ public struct PowerMetrics: Sendable { /// /// Air quality metrics -public struct AirQualityMetrics: Sendable { +public struct AirQualityMetrics { // 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. @@ -495,7 +501,7 @@ public struct AirQualityMetrics: Sendable { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry: Sendable { +public struct Telemetry { // 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. @@ -548,7 +554,7 @@ public struct Telemetry: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable, Sendable { + public enum OneOf_Variant: Equatable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -562,6 +568,32 @@ public struct Telemetry: Sendable { /// Power Metrics case powerMetrics(PowerMetrics) + #if !swift(>=4.1) + public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.deviceMetrics, .deviceMetrics): return { + guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.environmentMetrics, .environmentMetrics): return { + guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.airQualityMetrics, .airQualityMetrics): return { + guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.powerMetrics, .powerMetrics): return { + guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -569,7 +601,7 @@ public struct Telemetry: Sendable { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config: Sendable { +public struct Nau7802Config { // 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. @@ -587,6 +619,17 @@ public struct Nau7802Config: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension TelemetrySensorType: @unchecked Sendable {} +extension DeviceMetrics: @unchecked Sendable {} +extension EnvironmentMetrics: @unchecked Sendable {} +extension PowerMetrics: @unchecked Sendable {} +extension AirQualityMetrics: @unchecked Sendable {} +extension Telemetry: @unchecked Sendable {} +extension Telemetry.OneOf_Variant: @unchecked Sendable {} +extension Nau7802Config: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -652,13 +695,13 @@ extension DeviceMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.batteryLevel != 0 { try visitor.visitSingularUInt32Field(value: self.batteryLevel, fieldNumber: 1) } - if self.voltage.bitPattern != 0 { + if self.voltage != 0 { try visitor.visitSingularFloatField(value: self.voltage, fieldNumber: 2) } - if self.channelUtilization.bitPattern != 0 { + if self.channelUtilization != 0 { try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 3) } - if self.airUtilTx.bitPattern != 0 { + if self.airUtilTx != 0 { try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 4) } if self.uptimeSeconds != 0 { @@ -792,55 +835,55 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._temperature.bitPattern != 0 { + if _storage._temperature != 0 { try visitor.visitSingularFloatField(value: _storage._temperature, fieldNumber: 1) } - if _storage._relativeHumidity.bitPattern != 0 { + if _storage._relativeHumidity != 0 { try visitor.visitSingularFloatField(value: _storage._relativeHumidity, fieldNumber: 2) } - if _storage._barometricPressure.bitPattern != 0 { + if _storage._barometricPressure != 0 { try visitor.visitSingularFloatField(value: _storage._barometricPressure, fieldNumber: 3) } - if _storage._gasResistance.bitPattern != 0 { + if _storage._gasResistance != 0 { try visitor.visitSingularFloatField(value: _storage._gasResistance, fieldNumber: 4) } - if _storage._voltage.bitPattern != 0 { + if _storage._voltage != 0 { try visitor.visitSingularFloatField(value: _storage._voltage, fieldNumber: 5) } - if _storage._current.bitPattern != 0 { + if _storage._current != 0 { try visitor.visitSingularFloatField(value: _storage._current, fieldNumber: 6) } if _storage._iaq != 0 { try visitor.visitSingularUInt32Field(value: _storage._iaq, fieldNumber: 7) } - if _storage._distance.bitPattern != 0 { + if _storage._distance != 0 { try visitor.visitSingularFloatField(value: _storage._distance, fieldNumber: 8) } - if _storage._lux.bitPattern != 0 { + if _storage._lux != 0 { try visitor.visitSingularFloatField(value: _storage._lux, fieldNumber: 9) } - if _storage._whiteLux.bitPattern != 0 { + if _storage._whiteLux != 0 { try visitor.visitSingularFloatField(value: _storage._whiteLux, fieldNumber: 10) } - if _storage._irLux.bitPattern != 0 { + if _storage._irLux != 0 { try visitor.visitSingularFloatField(value: _storage._irLux, fieldNumber: 11) } - if _storage._uvLux.bitPattern != 0 { + if _storage._uvLux != 0 { try visitor.visitSingularFloatField(value: _storage._uvLux, fieldNumber: 12) } if _storage._windDirection != 0 { try visitor.visitSingularUInt32Field(value: _storage._windDirection, fieldNumber: 13) } - if _storage._windSpeed.bitPattern != 0 { + if _storage._windSpeed != 0 { try visitor.visitSingularFloatField(value: _storage._windSpeed, fieldNumber: 14) } - if _storage._weight.bitPattern != 0 { + if _storage._weight != 0 { try visitor.visitSingularFloatField(value: _storage._weight, fieldNumber: 15) } - if _storage._windGust.bitPattern != 0 { + if _storage._windGust != 0 { try visitor.visitSingularFloatField(value: _storage._windGust, fieldNumber: 16) } - if _storage._windLull.bitPattern != 0 { + if _storage._windLull != 0 { try visitor.visitSingularFloatField(value: _storage._windLull, fieldNumber: 17) } } @@ -907,22 +950,22 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } public func traverse(visitor: inout V) throws { - if self.ch1Voltage.bitPattern != 0 { + if self.ch1Voltage != 0 { try visitor.visitSingularFloatField(value: self.ch1Voltage, fieldNumber: 1) } - if self.ch1Current.bitPattern != 0 { + if self.ch1Current != 0 { try visitor.visitSingularFloatField(value: self.ch1Current, fieldNumber: 2) } - if self.ch2Voltage.bitPattern != 0 { + if self.ch2Voltage != 0 { try visitor.visitSingularFloatField(value: self.ch2Voltage, fieldNumber: 3) } - if self.ch2Current.bitPattern != 0 { + if self.ch2Current != 0 { try visitor.visitSingularFloatField(value: self.ch2Current, fieldNumber: 4) } - if self.ch3Voltage.bitPattern != 0 { + if self.ch3Voltage != 0 { try visitor.visitSingularFloatField(value: self.ch3Voltage, fieldNumber: 5) } - if self.ch3Current.bitPattern != 0 { + if self.ch3Current != 0 { try visitor.visitSingularFloatField(value: self.ch3Current, fieldNumber: 6) } try unknownFields.traverse(visitor: &visitor) @@ -1174,7 +1217,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor.bitPattern != 0 { + if self.calibrationFactor != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 89d0097c..1f41fe0b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem: @unchecked Sendable { +public struct XModem { // 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. @@ -35,7 +35,7 @@ public struct XModem: @unchecked Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Control: SwiftProtobuf.Enum { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,23 +79,34 @@ public struct XModem: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] - } public init() {} } +#if swift(>=4.2) + +extension XModem.Control: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension XModem: @unchecked Sendable {} +extension XModem.Control: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/protobufs b/protobufs index 97674883..d0fe91ab 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 976748839fafcf0049bb364fe2c7226a194d18a9 +Subproject commit d0fe91ab99734cacdc188403f73fe30f766917cf From 3f139244e1df83c21b8f9ea1b944d0a3f3cf3551 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 7 Aug 2024 09:43:48 -0700 Subject: [PATCH 053/333] Initial security config setup --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ .../contents | 12 +++++ .../Settings/Config/SecurityConfig.swift | 47 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 Meshtastic/Views/Settings/Config/SecurityConfig.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cca267e7..f3c62dc1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */; }; DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; + DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -292,6 +293,7 @@ DD1BD0EA2C601795008C0C70 /* CLLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocation.swift; sourceTree = ""; }; DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; + DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -680,6 +682,7 @@ DD2553582855B52700E55709 /* PositionConfig.swift */, DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */, D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */, + DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */, DD61937B2863877A00E59241 /* Module */, ); path = Config; @@ -1276,6 +1279,7 @@ DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, + DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index 6b2eaa00..741b1252 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -245,6 +245,7 @@ + @@ -334,6 +335,17 @@ + + + + + + + + + + + diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift new file mode 100644 index 00000000..57153785 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -0,0 +1,47 @@ +// +// Security.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/7/24. +// + +import Foundation +import SwiftUI +import CoreData +import MeshtasticProtobufs +import OSLog + +struct SecurityConfig: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + + var node: NodeInfoEntity? + + @State var hasChanges = false + @State var isManaged = false + @State var serialEnabled = true + @State var debugLogEnabled = false + @State var bluetoothLoggingEabled = false + @State var adminChannelEnabled = false + @State var publicKey = "" + @State var privateKey = "" + @State var adminKey = "" + + var body: some View { + VStack { + Form { + ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues) + } + } + } + func setSecurityValues() { + self.isManaged = node?.securityConfig?.isManaged ?? false + self.serialEnabled = node?.securityConfig?.serialEnabled ?? false + self.debugLogEnabled = node?.securityConfig?.debugLogEnabled ?? false + self.bluetoothLoggingEabled = node?.securityConfig?.bluetoothLoggingEabled ?? false + self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false + self.hasChanges = false + } +} From 9f999805343b46108337a1fd3221050b7ed601d5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 Aug 2024 07:33:31 -0700 Subject: [PATCH 054/333] initial security config proto mockup --- Localizable.xcstrings | 62 ++++++- Meshtastic/Helpers/BLEManager.swift | 31 ++++ .../contents | 6 +- Meshtastic/Persistence/Persistence.swift | 4 +- Meshtastic/Persistence/UpdateCoreData.swift | 49 ++++++ Meshtastic/Router/NavigationState.swift | 1 + Meshtastic/Views/ContentView.swift | 7 +- .../Messages/MessageContextMenuItems.swift | 6 +- Meshtastic/Views/Messages/UserList.swift | 6 + Meshtastic/Views/Nodes/NodeList.swift | 1 - .../Settings/Config/SecurityConfig.swift | 126 +++++++++++++- Meshtastic/Views/Settings/Settings.swift | 10 ++ .../Sources/meshtastic/config.pb.swift | 157 ++++++++++++++++++ .../Sources/meshtastic/localonly.pb.swift | 19 +++ .../Sources/meshtastic/mesh.pb.swift | 41 +++++ protobufs | 2 +- 16 files changed, 510 insertions(+), 18 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d80208dd..b704efe3 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -481,6 +481,12 @@ } } } + }, + "Admin & Direct Message Keys" : { + + }, + "Admin Key" : { + }, "admin.log" : { "extractionState" : "manual", @@ -705,6 +711,9 @@ }, "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth." : { + }, + "Allow incoming device control over the insecure legacy admin channel." : { + }, "Allow Position Requests" : { @@ -1843,6 +1852,9 @@ } } } + }, + "Bluetooth Logs" : { + }, "bluetooth.config" : { "localizations" : { @@ -5337,6 +5349,9 @@ } } } + }, + "Developer" : { + }, "Developers" : { @@ -5401,6 +5416,9 @@ }, "Device GPS" : { + }, + "Device is managed by a mesh administrator." : { + }, "Device Logging Enabled" : { @@ -6954,6 +6972,9 @@ } } } + }, + "Encrypted" : { + }, "Encryption Enabled" : { @@ -8252,7 +8273,7 @@ "Hops Away" : { }, - "Hops Away %d) dB" : { + "Hops Away %d" : { }, "Hops Away:" : { @@ -10843,6 +10864,9 @@ }, "LED State" : { + }, + "Legacy Administration" : { + }, "Licensed Operator" : { @@ -15976,6 +16000,9 @@ }, "Other data sources" : { + }, + "Output live debug logging over serial." : { + }, "Output pin buzzer GPIO " : { @@ -16626,9 +16653,15 @@ }, "Primary GPIO" : { + }, + "Private Key" : { + }, "Project information" : { + }, + "Public Key" : { + }, "PWD" : { @@ -18801,6 +18834,12 @@ }, "Secondary" : { + }, + "Security" : { + + }, + "Security Config" : { + }, "Select a channel" : { @@ -19016,6 +19055,9 @@ }, "Sensor Options" : { + }, + "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + }, "Sequence number" : { @@ -19083,6 +19125,12 @@ }, "Serial Console" : { + }, + "Serial Console over the Stream API." : { + + }, + "Serial Debug Logs" : { + }, "serial.config" : { "localizations" : { @@ -20916,6 +20964,9 @@ }, "The minimum distance change in meters to be considered for a smart position broadcast." : { + }, + "The public key authorized to send admin messages to this node." : { + }, "The region where you will be using your radios." : { @@ -22127,6 +22178,9 @@ }, "Use PWM Buzzer" : { + }, + "Used to create a shared key with a remote device." : { + }, "user" : { "localizations" : { @@ -22294,6 +22348,12 @@ }, "Via Mqtt" : { + }, + "View and export position-redacted device logs over Bluetooth" : { + + }, + "View Logs" : { + }, "voltage" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 475235de..13941ec9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1970,6 +1970,37 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } + public func saveSecurityConfig(config: Config.SecurityConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setConfig.security = config + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.channel = UInt32(adminIndex) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index 741b1252..a04f71ea 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -155,7 +155,9 @@ + + @@ -338,7 +340,7 @@ - + @@ -435,6 +437,8 @@ + + diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 93ba7f16..70e9f898 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -104,8 +104,8 @@ extension NSPersistentContainer { } /// Restore backup persistent stores located in the directory referenced by `backupURL`. - /// - /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. + /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. + /// When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. /// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores. /// - Throws: `CopyPersistentStoreError` in various situations. /// - Returns: Nothing. If no errors are thrown, the restore is complete. diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c407f414..843554b2 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -766,6 +766,55 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context } } +func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum)) + MeshLogger.log("🌐 \(logString)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Security Config + if !fetchedNode.isEmpty { + if fetchedNode[0].securityConfig == nil { + let newSecurityConfig = SecurityConfigEntity(context: context) + newSecurityConfig.publicKey = config.publicKey + newSecurityConfig.privateKey = config.privateKey + newSecurityConfig.adminKey = config.adminKey + newSecurityConfig.isManaged = config.isManaged + newSecurityConfig.serialEnabled = config.serialEnabled + newSecurityConfig.debugLogEnabled = config.debugLogEnabled + newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + } else { + fetchedNode[0].securityConfig?.publicKey = config.publicKey + fetchedNode[0].securityConfig?.privateKey = config.privateKey + fetchedNode[0].securityConfig?.adminKey = config.adminKey + fetchedNode[0].securityConfig?.isManaged = config.isManaged + fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled + fetchedNode[0].securityConfig?.debugLogEnabled = config.debugLogEnabled + fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + } + + do { + try context.save() + Logger.data.info("💾 [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [NetworkConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("💥 [NetworkConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Network Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("💥 [NetworkConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } +} + func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum)) diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index b33fc48a..0ff61108 100644 --- a/Meshtastic/Router/NavigationState.swift +++ b/Meshtastic/Router/NavigationState.swift @@ -46,6 +46,7 @@ enum SettingsNavigationState: String { case paxCounter case ringtone case serial + case security case storeAndForward case telemetry case meshLog diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index b109a318..4ab6ac85 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -6,11 +6,8 @@ import SwiftUI @available(iOS 17.0, *) struct ContentView: View { - @ObservedObject - var appState: AppState - - @ObservedObject - var router: Router + @ObservedObject var appState: AppState + @ObservedObject var router: Router var body: some View { TabView(selection: Binding( diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index ab363f59..7835a723 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -13,6 +13,9 @@ struct MessageContextMenuItems: View { var body: some View { VStack { + if message.pkiEncrypted { + Label("Encrypted", systemImage: "lock") + } Text("channel") + Text(": \(message.channel)") } @@ -53,6 +56,7 @@ struct MessageContextMenuItems: View { let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray) } + if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 { VStack { Text("SNR \(String(format: "%.2f", message.snr)) dB") @@ -60,7 +64,7 @@ struct MessageContextMenuItems: View { } } else if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) { VStack { - Text("Hops Away \(message.fromUser?.userNode?.hopsAway ?? 0)) dB") + Text("Hops Away \(message.fromUser?.userNode?.hopsAway ?? 0)") } } if isCurrentUser && message.receivedACK { diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 13eb837d..392ce4d0 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -32,6 +32,7 @@ struct UserList: View { @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "userNode.favorite", ascending: false), + NSSortDescriptor(key: "pkiEncrypted", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], animation: .default ) @@ -69,6 +70,10 @@ struct UserList: View { VStack(alignment: .leading) { HStack { + if user.pkiEncrypted { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } Text(user.longName ?? "unknown".localized) .font(.headline) Spacer() @@ -240,6 +245,7 @@ struct UserList: View { 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 diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c442b315..e05cb34b 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -37,7 +37,6 @@ struct NodeList: View { @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 - var boolFilters: [Bool] {[ isOnline, isFavorite, diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 57153785..47e56b18 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -20,27 +20,141 @@ struct SecurityConfig: View { var node: NodeInfoEntity? @State var hasChanges = false - @State var isManaged = false - @State var serialEnabled = true - @State var debugLogEnabled = false - @State var bluetoothLoggingEabled = false - @State var adminChannelEnabled = false @State var publicKey = "" @State var privateKey = "" @State var adminKey = "" + @State var isManaged = false + @State var serialEnabled = false + @State var debugLogEnabled = false + @State var bluetoothLoggingEnabled = false + @State var adminChannelEnabled = false var body: some View { VStack { Form { ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues) + + Section(header: Text("Admin & Direct Message Keys")) { + VStack(alignment: .leading) { + Label("Public Key", systemImage: "key") + + Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") + .foregroundStyle(.secondary) + .font(.caption) + + TextField( + "Public Key", + text: $publicKey, + axis: .vertical + ) + .padding(5) + .keyboardType(.alphabet) + .foregroundColor(.secondary) + .disableAutocorrection(true) + .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) + ) + + } + VStack(alignment: .leading) { + Label("Private Key", systemImage: "key.fill") + + Text("Used to create a shared key with a remote device.") + .foregroundStyle(.secondary) + .font(.caption) + + TextField( + "Private Key", + text: $privateKey, + axis: .vertical + ) + .padding(5) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .foregroundColor(.secondary) + .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) + ) + } + VStack(alignment: .leading) { + Label("Admin Key", systemImage: "key.viewfinder") + + Text("The public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(.caption) + + TextField( + "Admin Key", + text: $adminKey, + axis: .vertical + ) + .padding(5) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .foregroundColor(.secondary) + .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) + ) + } + } + Section(header: Text("Logs")) { + Toggle(isOn: $bluetoothLoggingEnabled) { + Label("Bluetooth Logs", systemImage: "dot.radiowaves.right") + Text("View and export position-redacted device logs over Bluetooth") + Link("View Logs", destination: URL(string: "meshtastic:///settings/debugLogs")!) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Section(header: Text("Administration")) { + if adminKey.length > 0 || adminChannelEnabled { + Toggle(isOn: $isManaged) { + Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") + Text("Device is managed by a mesh administrator.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Toggle(isOn: $adminChannelEnabled) { + Label("Legacy Administration", systemImage: "lock.slash") + Text("Allow incoming device control over the insecure legacy admin channel.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Section(header: Text("Developer")) { + Toggle(isOn: $serialEnabled) { + Label("Serial Console", systemImage: "terminal") + Text("Serial Console over the Stream API.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if serialEnabled { + Toggle(isOn: $debugLogEnabled) { + Label("Serial Debug Logs", systemImage: "ant.fill") + Text("Output live debug logging over serial.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + } } } + .navigationTitle("Security Config") + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + }) } func setSecurityValues() { + self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? "" + self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" + self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogEnabled = node?.securityConfig?.debugLogEnabled ?? false - self.bluetoothLoggingEabled = node?.securityConfig?.bluetoothLoggingEabled ?? false + self.bluetoothLoggingEnabled = node?.securityConfig?.bluetoothLoggingEnabled ?? false self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false self.hasChanges = false } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 880dabf2..85aafd8f 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -73,6 +73,14 @@ struct Settings: View { } .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + NavigationLink(value: SettingsNavigationState.security) { + Label { + Text("Security") + } icon: { + Image(systemName: "lock.shield") + } + } + NavigationLink(value: SettingsNavigationState.shareQRCode) { Label { Text("share.channels") @@ -461,6 +469,8 @@ struct Settings: View { PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) case .ringtone: RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) + case .security: + SecurityConfig(node: nodes.first(where: { $0.num == selectedNode })) case .serial: SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) case .storeAndForward: diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index f396367c..01e74689 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -85,6 +85,14 @@ public struct Config { set {payloadVariant = .bluetooth(newValue)} } + public var security: Config.SecurityConfig { + get { + if case .security(let v)? = payloadVariant {return v} + return Config.SecurityConfig() + } + set {payloadVariant = .security(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -97,6 +105,7 @@ public struct Config { case display(Config.DisplayConfig) case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) + case security(Config.SecurityConfig) #if !swift(>=4.1) public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { @@ -132,6 +141,10 @@ public struct Config { guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } return l == r }() + case (.security, .security): return { + guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -151,11 +164,13 @@ public struct Config { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI + /// Moved to SecurityConfig public var serialEnabled: Bool = false /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). /// Set this to true to leave the debug log outputting even when API is active. + /// Moved to SecurityConfig public var debugLogEnabled: Bool = false /// @@ -184,6 +199,7 @@ public struct Config { /// /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface + /// Moved to SecurityConfig public var isManaged: Bool = false /// @@ -1469,6 +1485,7 @@ public struct Config { /// /// Enables device (serial style logs) over Bluetooth + /// Moved to SecurityConfig public var deviceLoggingEnabled: Bool = false public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -1516,6 +1533,53 @@ public struct Config { public init() {} } + public struct SecurityConfig { + // 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. + + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + public var publicKey: Data = Data() + + /// + /// The private key of the device. + /// This is used to create a shared key with a remote device. + public var privateKey: Data = Data() + + /// + /// This is the public key authorized to send admin messages to this node + public var adminKey: Data = Data() + + /// + /// If true, device is considered to be "managed" by a mesh administrator + /// Clients should then limit available configuration and administrative options inside the user interface + public var isManaged: Bool = false + + /// + /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI + public var serialEnabled: Bool = false + + /// + /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). + /// Set this to true to leave the debug log outputting even when API is active. + public var debugLogEnabled: Bool = false + + /// + /// Enables device (serial style logs) over Bluetooth + /// Moved to SecurityConfig + public var bluetoothLoggingEnabled: Bool = false + + /// + /// Enables incoming admin control over the "admin" channel + public var adminChannelEnabled: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } @@ -1710,6 +1774,7 @@ extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} extension Config.BluetoothConfig: @unchecked Sendable {} extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} +extension Config.SecurityConfig: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -1726,6 +1791,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 5: .same(proto: "display"), 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), + 8: .same(proto: "security"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1825,6 +1891,19 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas self.payloadVariant = .bluetooth(v) } }() + case 8: try { + var v: Config.SecurityConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .security(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .security(v) + } + }() default: break } } @@ -1864,6 +1943,10 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas guard case .bluetooth(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 7) }() + case .security?: try { + guard case .security(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2777,3 +2860,77 @@ extension Config.BluetoothConfig.PairingMode: SwiftProtobuf._ProtoNameProviding 2: .same(proto: "NO_PIN"), ] } + +extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Config.protoMessageName + ".SecurityConfig" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "public_key"), + 2: .standard(proto: "private_key"), + 3: .standard(proto: "admin_key"), + 4: .standard(proto: "is_managed"), + 5: .standard(proto: "serial_enabled"), + 6: .standard(proto: "debug_log_enabled"), + 7: .standard(proto: "bluetooth_logging_enabled"), + 8: .standard(proto: "admin_channel_enabled"), + ] + + 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.decodeSingularBytesField(value: &self.publicKey) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.privateKey) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.adminKey) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() + case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogEnabled) }() + case 7: try { try decoder.decodeSingularBoolField(value: &self.bluetoothLoggingEnabled) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.adminChannelEnabled) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.publicKey.isEmpty { + try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 1) + } + if !self.privateKey.isEmpty { + try visitor.visitSingularBytesField(value: self.privateKey, fieldNumber: 2) + } + if !self.adminKey.isEmpty { + try visitor.visitSingularBytesField(value: self.adminKey, fieldNumber: 3) + } + if self.isManaged != false { + try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 4) + } + if self.serialEnabled != false { + try visitor.visitSingularBoolField(value: self.serialEnabled, fieldNumber: 5) + } + if self.debugLogEnabled != false { + try visitor.visitSingularBoolField(value: self.debugLogEnabled, fieldNumber: 6) + } + if self.bluetoothLoggingEnabled != false { + try visitor.visitSingularBoolField(value: self.bluetoothLoggingEnabled, fieldNumber: 7) + } + if self.adminChannelEnabled != false { + try visitor.visitSingularBoolField(value: self.adminChannelEnabled, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Config.SecurityConfig, rhs: Config.SecurityConfig) -> Bool { + if lhs.publicKey != rhs.publicKey {return false} + if lhs.privateKey != rhs.privateKey {return false} + if lhs.adminKey != rhs.adminKey {return false} + if lhs.isManaged != rhs.isManaged {return false} + if lhs.serialEnabled != rhs.serialEnabled {return false} + if lhs.debugLogEnabled != rhs.debugLogEnabled {return false} + if lhs.bluetoothLoggingEnabled != rhs.bluetoothLoggingEnabled {return false} + if lhs.adminChannelEnabled != rhs.adminChannelEnabled {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 5e30d1cd..0af27466 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -111,6 +111,17 @@ public struct LocalConfig { set {_uniqueStorage()._version = newValue} } + /// + /// The part of the config that is specific to Security settings + public var security: Config.SecurityConfig { + get {return _storage._security ?? Config.SecurityConfig()} + set {_uniqueStorage()._security = newValue} + } + /// Returns true if `security` has been explicitly set. + public var hasSecurity: Bool {return _storage._security != nil} + /// Clears the value of `security`. Subsequent reads from it will return its default value. + public mutating func clearSecurity() {_uniqueStorage()._security = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -302,6 +313,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), 8: .same(proto: "version"), + 9: .same(proto: "security"), ] fileprivate class _StorageClass { @@ -313,6 +325,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _lora: Config.LoRaConfig? = nil var _bluetooth: Config.BluetoothConfig? = nil var _version: UInt32 = 0 + var _security: Config.SecurityConfig? = nil #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -335,6 +348,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati _lora = source._lora _bluetooth = source._bluetooth _version = source._version + _security = source._security } } @@ -361,6 +375,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati case 6: try { try decoder.decodeSingularMessageField(value: &_storage._lora) }() case 7: try { try decoder.decodeSingularMessageField(value: &_storage._bluetooth) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }() + case 9: try { try decoder.decodeSingularMessageField(value: &_storage._security) }() default: break } } @@ -397,6 +412,9 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if _storage._version != 0 { try visitor.visitSingularUInt32Field(value: _storage._version, fieldNumber: 8) } + try { if let v = _storage._security { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -414,6 +432,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if _storage._lora != rhs_storage._lora {return false} if _storage._bluetooth != rhs_storage._bluetooth {return false} if _storage._version != rhs_storage._version {return false} + if _storage._security != rhs_storage._security {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 b12cc13f..f0a7c55c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1158,6 +1158,11 @@ public struct User { /// Indicates that the user's role in the mesh public var role: Config.DeviceConfig.Role = .client + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + public var publicKey: Data = Data() + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1714,6 +1719,20 @@ public struct MeshPacket { set {_uniqueStorage()._hopStart = newValue} } + /// + /// Records the public key the packet was encrypted with, if applicable. + public var publicKey: Data { + get {return _storage._publicKey} + set {_uniqueStorage()._publicKey = newValue} + } + + /// + /// Indicates whether the packet was en/decrypted using PKI + public var pkiEncrypted: Bool { + get {return _storage._pkiEncrypted} + set {_uniqueStorage()._pkiEncrypted = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_PayloadVariant: Equatable { @@ -3367,6 +3386,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, 5: .standard(proto: "hw_model"), 6: .standard(proto: "is_licensed"), 7: .same(proto: "role"), + 8: .standard(proto: "public_key"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3382,6 +3402,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, case 5: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 8: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() default: break } } @@ -3409,6 +3430,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if self.role != .client { try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7) } + if !self.publicKey.isEmpty { + try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } @@ -3420,6 +3444,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if lhs.hwModel != rhs.hwModel {return false} if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} + if lhs.publicKey != rhs.publicKey {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3795,6 +3820,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 13: .same(proto: "delayed"), 14: .standard(proto: "via_mqtt"), 15: .standard(proto: "hop_start"), + 16: .standard(proto: "public_key"), + 17: .standard(proto: "pki_encrypted"), ] fileprivate class _StorageClass { @@ -3812,6 +3839,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _delayed: MeshPacket.Delayed = .noDelay var _viaMqtt: Bool = false var _hopStart: UInt32 = 0 + var _publicKey: Data = Data() + var _pkiEncrypted: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -3840,6 +3869,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio _delayed = source._delayed _viaMqtt = source._viaMqtt _hopStart = source._hopStart + _publicKey = source._publicKey + _pkiEncrypted = source._pkiEncrypted } } @@ -3892,6 +3923,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 13: try { try decoder.decodeSingularEnumField(value: &_storage._delayed) }() case 14: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() case 15: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopStart) }() + case 16: try { try decoder.decodeSingularBytesField(value: &_storage._publicKey) }() + case 17: try { try decoder.decodeSingularBoolField(value: &_storage._pkiEncrypted) }() default: break } } @@ -3954,6 +3987,12 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._hopStart != 0 { try visitor.visitSingularUInt32Field(value: _storage._hopStart, fieldNumber: 15) } + if !_storage._publicKey.isEmpty { + try visitor.visitSingularBytesField(value: _storage._publicKey, fieldNumber: 16) + } + if _storage._pkiEncrypted != false { + try visitor.visitSingularBoolField(value: _storage._pkiEncrypted, fieldNumber: 17) + } } try unknownFields.traverse(visitor: &visitor) } @@ -3977,6 +4016,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._delayed != rhs_storage._delayed {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopStart != rhs_storage._hopStart {return false} + if _storage._publicKey != rhs_storage._publicKey {return false} + if _storage._pkiEncrypted != rhs_storage._pkiEncrypted {return false} return true } if !storagesAreEqual {return false} diff --git a/protobufs b/protobufs index d0fe91ab..81fd9d37 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit d0fe91ab99734cacdc188403f73fe30f766917cf +Subproject commit 81fd9d374f1b364c2812b7a0ca2784a7605b6835 From a274d6fcf8f03e6ef0ebb6d31dd24d0c3bfd4ed6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 Aug 2024 10:39:45 -0700 Subject: [PATCH 055/333] Update protos --- Meshtastic/Helpers/BLEManager.swift | 2 +- .../contents | 2 +- Meshtastic/Persistence/UpdateCoreData.swift | 4 +- .../Settings/Config/SecurityConfig.swift | 6 +- .../Sources/meshtastic/admin.pb.swift | 54 +- .../Sources/meshtastic/config.pb.swift | 40 +- .../Sources/meshtastic/mesh.pb.swift | 277 +++++-- .../Sources/meshtastic/telemetry.pb.swift | 717 ++++++++++++------ protobufs | 2 +- 9 files changed, 799 insertions(+), 305 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 13941ec9..ec03b2f8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1388,7 +1388,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() - adminPacket.factoryReset = 5 + adminPacket.factoryResetConfig = 5 var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index a04f71ea..432be802 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -341,7 +341,7 @@ - + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 843554b2..304c6a77 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -785,7 +785,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, c newSecurityConfig.adminKey = config.adminKey newSecurityConfig.isManaged = config.isManaged newSecurityConfig.serialEnabled = config.serialEnabled - newSecurityConfig.debugLogEnabled = config.debugLogEnabled + newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled } else { fetchedNode[0].securityConfig?.publicKey = config.publicKey @@ -793,7 +793,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, c fetchedNode[0].securityConfig?.adminKey = config.adminKey fetchedNode[0].securityConfig?.isManaged = config.isManaged fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled - fetchedNode[0].securityConfig?.debugLogEnabled = config.debugLogEnabled + fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 47e56b18..4c8bd85a 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -25,7 +25,7 @@ struct SecurityConfig: View { @State var adminKey = "" @State var isManaged = false @State var serialEnabled = false - @State var debugLogEnabled = false + @State var debugLogApiEnabled = false @State var bluetoothLoggingEnabled = false @State var adminChannelEnabled = false @@ -132,7 +132,7 @@ struct SecurityConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if serialEnabled { - Toggle(isOn: $debugLogEnabled) { + Toggle(isOn: $debugLogApiEnabled) { Label("Serial Debug Logs", systemImage: "ant.fill") Text("Output live debug logging over serial.") } @@ -153,7 +153,7 @@ struct SecurityConfig: View { self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false - self.debugLogEnabled = node?.securityConfig?.debugLogEnabled ?? false + self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false self.bluetoothLoggingEnabled = node?.securityConfig?.bluetoothLoggingEnabled ?? false self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false self.hasChanges = false diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index ba263709..f526f4ca 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -390,6 +390,16 @@ public struct AdminMessage { set {payloadVariant = .commitEditSettings(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 { + get { + if case .factoryResetDevice(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .factoryResetDevice(newValue)} + } + /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. @@ -433,13 +443,13 @@ public struct AdminMessage { } /// - /// Tell the node to factory reset, all device settings will be returned to factory defaults. - public var factoryReset: Int32 { + /// Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. + public var factoryResetConfig: Int32 { get { - if case .factoryReset(let v)? = payloadVariant {return v} + if case .factoryResetConfig(let v)? = payloadVariant {return v} return 0 } - set {payloadVariant = .factoryReset(newValue)} + set {payloadVariant = .factoryResetConfig(newValue)} } /// @@ -570,6 +580,9 @@ public struct AdminMessage { /// Commits an open transaction for any edits made to config, module config, owner, and channel settings case commitEditSettings(Bool) /// + /// 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) + /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. case rebootOtaSeconds(Int32) @@ -584,8 +597,8 @@ public struct AdminMessage { /// Tell the node to shutdown in this many seconds (or <0 to cancel shutdown) case shutdownSeconds(Int32) /// - /// Tell the node to factory reset, all device settings will be returned to factory defaults. - case factoryReset(Int32) + /// Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. + case factoryResetConfig(Int32) /// /// Tell the node to reset the nodedb. case nodedbReset(Int32) @@ -736,6 +749,10 @@ public struct AdminMessage { guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } return l == r }() + case (.factoryResetDevice, .factoryResetDevice): return { + guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.rebootOtaSeconds, .rebootOtaSeconds): return { guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } return l == r @@ -752,8 +769,8 @@ public struct AdminMessage { guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } return l == r }() - case (.factoryReset, .factoryReset): return { - guard case .factoryReset(let l) = lhs, case .factoryReset(let r) = rhs else { preconditionFailure() } + case (.factoryResetConfig, .factoryResetConfig): return { + guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } return l == r }() case (.nodedbReset, .nodedbReset): return { @@ -1070,11 +1087,12 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 42: .standard(proto: "remove_fixed_position"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), + 94: .standard(proto: "factory_reset_device"), 95: .standard(proto: "reboot_ota_seconds"), 96: .standard(proto: "exit_simulator"), 97: .standard(proto: "reboot_seconds"), 98: .standard(proto: "shutdown_seconds"), - 99: .standard(proto: "factory_reset"), + 99: .standard(proto: "factory_reset_config"), 100: .standard(proto: "nodedb_reset"), ] @@ -1429,6 +1447,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .commitEditSettings(v) } }() + case 94: try { + var v: Int32? + try decoder.decodeSingularInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .factoryResetDevice(v) + } + }() case 95: try { var v: Int32? try decoder.decodeSingularInt32Field(value: &v) @@ -1466,7 +1492,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try decoder.decodeSingularInt32Field(value: &v) if let v = v { if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} - self.payloadVariant = .factoryReset(v) + self.payloadVariant = .factoryResetConfig(v) } }() case 100: try { @@ -1628,6 +1654,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .commitEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 65) }() + case .factoryResetDevice?: try { + guard case .factoryResetDevice(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularInt32Field(value: v, fieldNumber: 94) + }() case .rebootOtaSeconds?: try { guard case .rebootOtaSeconds(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 95) @@ -1644,8 +1674,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .shutdownSeconds(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 98) }() - case .factoryReset?: try { - guard case .factoryReset(let v)? = self.payloadVariant else { preconditionFailure() } + case .factoryResetConfig?: try { + guard case .factoryResetConfig(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 99) }() case .nodedbReset?: try { diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 01e74689..4b953470 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1404,6 +1404,7 @@ public struct Config { /// /// Very Long Range - Slow + /// Deprecated in 2.5: Works only with txco and is unusably slow case veryLongSlow // = 2 /// @@ -1425,6 +1426,12 @@ public struct Config { /// /// Long Range - Moderately Fast case longModerate // = 7 + + /// + /// Short Range - Turbo + /// This is the fastest preset and the only one with 500kHz bandwidth. + /// It is not legal to use in all regions due to this wider bandwidth. + case shortTurbo // = 8 case UNRECOGNIZED(Int) public init() { @@ -1441,6 +1448,7 @@ public struct Config { case 5: self = .shortSlow case 6: self = .shortFast case 7: self = .longModerate + case 8: self = .shortTurbo default: self = .UNRECOGNIZED(rawValue) } } @@ -1455,6 +1463,7 @@ public struct Config { case .shortSlow: return 5 case .shortFast: return 6 case .longModerate: return 7 + case .shortTurbo: return 8 case .UNRECOGNIZED(let i): return i } } @@ -1540,39 +1549,38 @@ public struct Config { /// /// The public key of the user's device. - /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + /// Sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() /// /// The private key of the device. - /// This is used to create a shared key with a remote device. + /// Used to create a shared key with a remote device. public var privateKey: Data = Data() /// - /// This is the public key authorized to send admin messages to this node + /// The public key authorized to send admin messages to this node. public var adminKey: Data = Data() /// - /// If true, device is considered to be "managed" by a mesh administrator - /// Clients should then limit available configuration and administrative options inside the user interface + /// If true, device is considered to be "managed" by a mesh administrator via admin messages + /// Device is managed by a mesh administrator. public var isManaged: Bool = false /// - /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI + /// Serial Console over the Stream API." public var serialEnabled: Bool = false /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Set this to true to leave the debug log outputting even when API is active. - public var debugLogEnabled: Bool = false + /// Output live debug logging over serial. + public var debugLogApiEnabled: Bool = false /// /// Enables device (serial style logs) over Bluetooth - /// Moved to SecurityConfig public var bluetoothLoggingEnabled: Bool = false /// - /// Enables incoming admin control over the "admin" channel + /// Allow incoming device control over the insecure legacy admin channel. public var adminChannelEnabled: Bool = false public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -1736,6 +1744,7 @@ extension Config.LoRaConfig.ModemPreset: CaseIterable { .shortSlow, .shortFast, .longModerate, + .shortTurbo, ] } @@ -2800,6 +2809,7 @@ extension Config.LoRaConfig.ModemPreset: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "SHORT_SLOW"), 6: .same(proto: "SHORT_FAST"), 7: .same(proto: "LONG_MODERATE"), + 8: .same(proto: "SHORT_TURBO"), ] } @@ -2869,7 +2879,7 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 3: .standard(proto: "admin_key"), 4: .standard(proto: "is_managed"), 5: .standard(proto: "serial_enabled"), - 6: .standard(proto: "debug_log_enabled"), + 6: .standard(proto: "debug_log_api_enabled"), 7: .standard(proto: "bluetooth_logging_enabled"), 8: .standard(proto: "admin_channel_enabled"), ] @@ -2885,7 +2895,7 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 3: try { try decoder.decodeSingularBytesField(value: &self.adminKey) }() case 4: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() case 5: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() - case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogEnabled) }() + case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogApiEnabled) }() case 7: try { try decoder.decodeSingularBoolField(value: &self.bluetoothLoggingEnabled) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.adminChannelEnabled) }() default: break @@ -2909,8 +2919,8 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.serialEnabled != false { try visitor.visitSingularBoolField(value: self.serialEnabled, fieldNumber: 5) } - if self.debugLogEnabled != false { - try visitor.visitSingularBoolField(value: self.debugLogEnabled, fieldNumber: 6) + if self.debugLogApiEnabled != false { + try visitor.visitSingularBoolField(value: self.debugLogApiEnabled, fieldNumber: 6) } if self.bluetoothLoggingEnabled != false { try visitor.visitSingularBoolField(value: self.bluetoothLoggingEnabled, fieldNumber: 7) @@ -2927,7 +2937,7 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.adminKey != rhs.adminKey {return false} if lhs.isManaged != rhs.isManaged {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} - if lhs.debugLogEnabled != rhs.debugLogEnabled {return false} + if lhs.debugLogApiEnabled != rhs.debugLogApiEnabled {return false} if lhs.bluetoothLoggingEnabled != rhs.bluetoothLoggingEnabled {return false} if lhs.adminChannelEnabled != rhs.adminChannelEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index f0a7c55c..489cd8e3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -782,23 +782,35 @@ public struct Position { /// The new preferred location encoding, multiply by 1e-7 to get degrees /// in floating point public var latitudeI: Int32 { - get {return _storage._latitudeI} + get {return _storage._latitudeI ?? 0} set {_uniqueStorage()._latitudeI = newValue} } + /// Returns true if `latitudeI` has been explicitly set. + public var hasLatitudeI: Bool {return _storage._latitudeI != nil} + /// Clears the value of `latitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLatitudeI() {_uniqueStorage()._latitudeI = nil} /// /// TODO: REPLACE public var longitudeI: Int32 { - get {return _storage._longitudeI} + get {return _storage._longitudeI ?? 0} set {_uniqueStorage()._longitudeI = newValue} } + /// Returns true if `longitudeI` has been explicitly set. + public var hasLongitudeI: Bool {return _storage._longitudeI != nil} + /// Clears the value of `longitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLongitudeI() {_uniqueStorage()._longitudeI = nil} /// /// In meters above MSL (but see issue #359) public var altitude: Int32 { - get {return _storage._altitude} + get {return _storage._altitude ?? 0} set {_uniqueStorage()._altitude = newValue} } + /// Returns true if `altitude` has been explicitly set. + public var hasAltitude: Bool {return _storage._altitude != nil} + /// Clears the value of `altitude`. Subsequent reads from it will return its default value. + public mutating func clearAltitude() {_uniqueStorage()._altitude = nil} /// /// This is usually not sent over the mesh (to save space), but it is sent @@ -841,16 +853,24 @@ public struct Position { /// /// HAE altitude in meters - can be used instead of MSL altitude public var altitudeHae: Int32 { - get {return _storage._altitudeHae} + get {return _storage._altitudeHae ?? 0} set {_uniqueStorage()._altitudeHae = newValue} } + /// Returns true if `altitudeHae` has been explicitly set. + public var hasAltitudeHae: Bool {return _storage._altitudeHae != nil} + /// Clears the value of `altitudeHae`. Subsequent reads from it will return its default value. + public mutating func clearAltitudeHae() {_uniqueStorage()._altitudeHae = nil} /// /// Geoidal separation in meters public var altitudeGeoidalSeparation: Int32 { - get {return _storage._altitudeGeoidalSeparation} + get {return _storage._altitudeGeoidalSeparation ?? 0} set {_uniqueStorage()._altitudeGeoidalSeparation = newValue} } + /// Returns true if `altitudeGeoidalSeparation` has been explicitly set. + public var hasAltitudeGeoidalSeparation: Bool {return _storage._altitudeGeoidalSeparation != nil} + /// Clears the value of `altitudeGeoidalSeparation`. Subsequent reads from it will return its default value. + public mutating func clearAltitudeGeoidalSeparation() {_uniqueStorage()._altitudeGeoidalSeparation = nil} /// /// Horizontal, Vertical and Position Dilution of Precision, in 1/100 units @@ -894,16 +914,24 @@ public struct Position { /// - "yaw" indicates a relative rotation about the vertical axis /// TODO: REMOVE/INTEGRATE public var groundSpeed: UInt32 { - get {return _storage._groundSpeed} + get {return _storage._groundSpeed ?? 0} set {_uniqueStorage()._groundSpeed = newValue} } + /// Returns true if `groundSpeed` has been explicitly set. + public var hasGroundSpeed: Bool {return _storage._groundSpeed != nil} + /// Clears the value of `groundSpeed`. Subsequent reads from it will return its default value. + public mutating func clearGroundSpeed() {_uniqueStorage()._groundSpeed = nil} /// /// TODO: REPLACE public var groundTrack: UInt32 { - get {return _storage._groundTrack} + get {return _storage._groundTrack ?? 0} set {_uniqueStorage()._groundTrack = newValue} } + /// Returns true if `groundTrack` has been explicitly set. + public var hasGroundTrack: Bool {return _storage._groundTrack != nil} + /// Clears the value of `groundTrack`. Subsequent reads from it will return its default value. + public mutating func clearGroundTrack() {_uniqueStorage()._groundTrack = nil} /// /// GPS fix quality (from NMEA GxGGA statement or similar) @@ -1455,11 +1483,25 @@ public struct Waypoint { /// /// latitude_i - public var latitudeI: Int32 = 0 + public var latitudeI: Int32 { + get {return _latitudeI ?? 0} + set {_latitudeI = newValue} + } + /// Returns true if `latitudeI` has been explicitly set. + public var hasLatitudeI: Bool {return self._latitudeI != nil} + /// Clears the value of `latitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLatitudeI() {self._latitudeI = nil} /// /// longitude_i - public var longitudeI: Int32 = 0 + public var longitudeI: Int32 { + get {return _longitudeI ?? 0} + set {_longitudeI = newValue} + } + /// Returns true if `longitudeI` has been explicitly set. + public var hasLongitudeI: Bool {return self._longitudeI != nil} + /// Clears the value of `longitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLongitudeI() {self._longitudeI = nil} /// /// Time the waypoint is to expire (epoch) @@ -1485,6 +1527,9 @@ public struct Waypoint { public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _latitudeI: Int32? = nil + fileprivate var _longitudeI: Int32? = nil } /// @@ -2369,6 +2414,16 @@ public struct FromRadio { set {payloadVariant = .fileInfo(newValue)} } + /// + /// Notification message to the client + public var clientNotification: ClientNotification { + get { + if case .clientNotification(let v)? = payloadVariant {return v} + return ClientNotification() + } + set {payloadVariant = .clientNotification(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -2424,6 +2479,9 @@ public struct FromRadio { /// /// File system manifest messages case fileInfo(FileInfo) + /// + /// Notification message to the client + case clientNotification(ClientNotification) #if !swift(>=4.1) public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { @@ -2487,6 +2545,10 @@ public struct FromRadio { guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } return l == r }() + case (.clientNotification, .clientNotification): return { + guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -2496,6 +2558,46 @@ public struct FromRadio { public init() {} } +/// +/// A notification message from the device to the client +/// To be used for important messages that should to be displayed to the user +/// in the form of push notifications or validation messages when saving +/// invalid configuration. +public struct ClientNotification { + // 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. + + /// + /// The id of the packet we're notifying in response to + public var replyID: UInt32 { + get {return _replyID ?? 0} + set {_replyID = newValue} + } + /// Returns true if `replyID` has been explicitly set. + public var hasReplyID: Bool {return self._replyID != nil} + /// Clears the value of `replyID`. Subsequent reads from it will return its default value. + public mutating func clearReplyID() {self._replyID = nil} + + /// + /// Seconds since 1970 - or 0 for unknown/unset + public var time: UInt32 = 0 + + /// + /// The level type of notification + public var level: LogRecord.Level = .unset + + /// + /// The message body of the notification + public var message: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _replyID: UInt32? = nil +} + /// /// Individual File info for the device public struct FileInfo { @@ -2987,6 +3089,7 @@ extension LogRecord.Level: @unchecked Sendable {} extension QueueStatus: @unchecked Sendable {} extension FromRadio: @unchecked Sendable {} extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension ClientNotification: @unchecked Sendable {} extension FileInfo: @unchecked Sendable {} extension ToRadio: @unchecked Sendable {} extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} @@ -3138,22 +3241,22 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB ] fileprivate class _StorageClass { - var _latitudeI: Int32 = 0 - var _longitudeI: Int32 = 0 - var _altitude: Int32 = 0 + var _latitudeI: Int32? = nil + var _longitudeI: Int32? = nil + var _altitude: Int32? = nil var _time: UInt32 = 0 var _locationSource: Position.LocSource = .locUnset var _altitudeSource: Position.AltSource = .altUnset var _timestamp: UInt32 = 0 var _timestampMillisAdjust: Int32 = 0 - var _altitudeHae: Int32 = 0 - var _altitudeGeoidalSeparation: Int32 = 0 + var _altitudeHae: Int32? = nil + var _altitudeGeoidalSeparation: Int32? = nil var _pdop: UInt32 = 0 var _hdop: UInt32 = 0 var _vdop: UInt32 = 0 var _gpsAccuracy: UInt32 = 0 - var _groundSpeed: UInt32 = 0 - var _groundTrack: UInt32 = 0 + var _groundSpeed: UInt32? = nil + var _groundTrack: UInt32? = nil var _fixQuality: UInt32 = 0 var _fixType: UInt32 = 0 var _satsInView: UInt32 = 0 @@ -3247,15 +3350,19 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._latitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: _storage._latitudeI, fieldNumber: 1) - } - if _storage._longitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: _storage._longitudeI, fieldNumber: 2) - } - if _storage._altitude != 0 { - try visitor.visitSingularInt32Field(value: _storage._altitude, fieldNumber: 3) - } + // 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 + try { if let v = _storage._latitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._longitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._altitude { + try visitor.visitSingularInt32Field(value: v, fieldNumber: 3) + } }() if _storage._time != 0 { try visitor.visitSingularFixed32Field(value: _storage._time, fieldNumber: 4) } @@ -3271,12 +3378,12 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._timestampMillisAdjust != 0 { try visitor.visitSingularInt32Field(value: _storage._timestampMillisAdjust, fieldNumber: 8) } - if _storage._altitudeHae != 0 { - try visitor.visitSingularSInt32Field(value: _storage._altitudeHae, fieldNumber: 9) - } - if _storage._altitudeGeoidalSeparation != 0 { - try visitor.visitSingularSInt32Field(value: _storage._altitudeGeoidalSeparation, fieldNumber: 10) - } + try { if let v = _storage._altitudeHae { + try visitor.visitSingularSInt32Field(value: v, fieldNumber: 9) + } }() + try { if let v = _storage._altitudeGeoidalSeparation { + try visitor.visitSingularSInt32Field(value: v, fieldNumber: 10) + } }() if _storage._pdop != 0 { try visitor.visitSingularUInt32Field(value: _storage._pdop, fieldNumber: 11) } @@ -3289,12 +3396,12 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._gpsAccuracy != 0 { try visitor.visitSingularUInt32Field(value: _storage._gpsAccuracy, fieldNumber: 14) } - if _storage._groundSpeed != 0 { - try visitor.visitSingularUInt32Field(value: _storage._groundSpeed, fieldNumber: 15) - } - if _storage._groundTrack != 0 { - try visitor.visitSingularUInt32Field(value: _storage._groundTrack, fieldNumber: 16) - } + try { if let v = _storage._groundSpeed { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 15) + } }() + try { if let v = _storage._groundTrack { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 16) + } }() if _storage._fixQuality != 0 { try visitor.visitSingularUInt32Field(value: _storage._fixQuality, fieldNumber: 17) } @@ -3676,8 +3783,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.id) }() - case 2: try { try decoder.decodeSingularSFixed32Field(value: &self.latitudeI) }() - case 3: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() + case 2: try { try decoder.decodeSingularSFixed32Field(value: &self._latitudeI) }() + case 3: try { try decoder.decodeSingularSFixed32Field(value: &self._longitudeI) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self.expire) }() case 5: try { try decoder.decodeSingularUInt32Field(value: &self.lockedTo) }() case 6: try { try decoder.decodeSingularStringField(value: &self.name) }() @@ -3689,15 +3796,19 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB } 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.id != 0 { try visitor.visitSingularUInt32Field(value: self.id, fieldNumber: 1) } - if self.latitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: self.latitudeI, fieldNumber: 2) - } - if self.longitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: self.longitudeI, fieldNumber: 3) - } + try { if let v = self._latitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._longitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 3) + } }() if self.expire != 0 { try visitor.visitSingularUInt32Field(value: self.expire, fieldNumber: 4) } @@ -3718,8 +3829,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static func ==(lhs: Waypoint, rhs: Waypoint) -> Bool { if lhs.id != rhs.id {return false} - if lhs.latitudeI != rhs.latitudeI {return false} - if lhs.longitudeI != rhs.longitudeI {return false} + if lhs._latitudeI != rhs._latitudeI {return false} + if lhs._longitudeI != rhs._longitudeI {return false} if lhs.expire != rhs.expire {return false} if lhs.lockedTo != rhs.lockedTo {return false} if lhs.name != rhs.name {return false} @@ -4369,6 +4480,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 13: .same(proto: "metadata"), 14: .same(proto: "mqttClientProxyMessage"), 15: .same(proto: "fileInfo"), + 16: .same(proto: "clientNotification"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4550,6 +4662,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.payloadVariant = .fileInfo(v) } }() + case 16: try { + var v: ClientNotification? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .clientNotification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .clientNotification(v) + } + }() default: break } } @@ -4620,6 +4745,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .fileInfo(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 15) }() + case .clientNotification?: try { + guard case .clientNotification(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 16) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -4633,6 +4762,60 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation } } +extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ClientNotification" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "reply_id"), + 2: .same(proto: "time"), + 3: .same(proto: "level"), + 4: .same(proto: "message"), + ] + + 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._replyID) }() + 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) }() + 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 + try { if let v = self._replyID { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + if self.time != 0 { + try visitor.visitSingularFixed32Field(value: self.time, fieldNumber: 2) + } + if self.level != .unset { + try visitor.visitSingularEnumField(value: self.level, fieldNumber: 3) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: ClientNotification, rhs: ClientNotification) -> Bool { + if lhs._replyID != rhs._replyID {return false} + if lhs.time != rhs.time {return false} + if lhs.level != rhs.level {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension FileInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FileInfo" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index ec627e3d..de4e550c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -245,27 +245,68 @@ public struct DeviceMetrics { /// /// 0-100 (>100 means powered) - public var batteryLevel: UInt32 = 0 + public var batteryLevel: UInt32 { + get {return _batteryLevel ?? 0} + set {_batteryLevel = newValue} + } + /// Returns true if `batteryLevel` has been explicitly set. + public var hasBatteryLevel: Bool {return self._batteryLevel != nil} + /// Clears the value of `batteryLevel`. Subsequent reads from it will return its default value. + public mutating func clearBatteryLevel() {self._batteryLevel = nil} /// /// Voltage measured - public var voltage: Float = 0 + public var voltage: Float { + get {return _voltage ?? 0} + set {_voltage = newValue} + } + /// Returns true if `voltage` has been explicitly set. + public var hasVoltage: Bool {return self._voltage != nil} + /// Clears the value of `voltage`. Subsequent reads from it will return its default value. + public mutating func clearVoltage() {self._voltage = nil} /// /// Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). - public var channelUtilization: Float = 0 + public var channelUtilization: Float { + get {return _channelUtilization ?? 0} + set {_channelUtilization = newValue} + } + /// Returns true if `channelUtilization` has been explicitly set. + public var hasChannelUtilization: Bool {return self._channelUtilization != nil} + /// Clears the value of `channelUtilization`. Subsequent reads from it will return its default value. + public mutating func clearChannelUtilization() {self._channelUtilization = nil} /// /// Percent of airtime for transmission used within the last hour. - public var airUtilTx: Float = 0 + public var airUtilTx: Float { + get {return _airUtilTx ?? 0} + set {_airUtilTx = newValue} + } + /// Returns true if `airUtilTx` has been explicitly set. + public var hasAirUtilTx: Bool {return self._airUtilTx != nil} + /// Clears the value of `airUtilTx`. Subsequent reads from it will return its default value. + public mutating func clearAirUtilTx() {self._airUtilTx = nil} /// /// How long the device has been running since the last reboot (in seconds) - public var uptimeSeconds: UInt32 = 0 + public var uptimeSeconds: UInt32 { + get {return _uptimeSeconds ?? 0} + set {_uptimeSeconds = newValue} + } + /// Returns true if `uptimeSeconds` has been explicitly set. + public var hasUptimeSeconds: Bool {return self._uptimeSeconds != nil} + /// Clears the value of `uptimeSeconds`. Subsequent reads from it will return its default value. + public mutating func clearUptimeSeconds() {self._uptimeSeconds = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _batteryLevel: UInt32? = nil + fileprivate var _voltage: Float? = nil + fileprivate var _channelUtilization: Float? = nil + fileprivate var _airUtilTx: Float? = nil + fileprivate var _uptimeSeconds: UInt32? = nil } /// @@ -278,123 +319,191 @@ public struct EnvironmentMetrics { /// /// Temperature measured public var temperature: Float { - get {return _storage._temperature} + get {return _storage._temperature ?? 0} set {_uniqueStorage()._temperature = newValue} } + /// Returns true if `temperature` has been explicitly set. + public var hasTemperature: Bool {return _storage._temperature != nil} + /// Clears the value of `temperature`. Subsequent reads from it will return its default value. + public mutating func clearTemperature() {_uniqueStorage()._temperature = nil} /// /// Relative humidity percent measured public var relativeHumidity: Float { - get {return _storage._relativeHumidity} + get {return _storage._relativeHumidity ?? 0} set {_uniqueStorage()._relativeHumidity = newValue} } + /// Returns true if `relativeHumidity` has been explicitly set. + public var hasRelativeHumidity: Bool {return _storage._relativeHumidity != nil} + /// Clears the value of `relativeHumidity`. Subsequent reads from it will return its default value. + public mutating func clearRelativeHumidity() {_uniqueStorage()._relativeHumidity = nil} /// /// Barometric pressure in hPA measured public var barometricPressure: Float { - get {return _storage._barometricPressure} + get {return _storage._barometricPressure ?? 0} set {_uniqueStorage()._barometricPressure = newValue} } + /// Returns true if `barometricPressure` has been explicitly set. + public var hasBarometricPressure: Bool {return _storage._barometricPressure != nil} + /// Clears the value of `barometricPressure`. Subsequent reads from it will return its default value. + public mutating func clearBarometricPressure() {_uniqueStorage()._barometricPressure = nil} /// /// Gas resistance in MOhm measured public var gasResistance: Float { - get {return _storage._gasResistance} + get {return _storage._gasResistance ?? 0} set {_uniqueStorage()._gasResistance = newValue} } + /// Returns true if `gasResistance` has been explicitly set. + public var hasGasResistance: Bool {return _storage._gasResistance != nil} + /// Clears the value of `gasResistance`. Subsequent reads from it will return its default value. + public mutating func clearGasResistance() {_uniqueStorage()._gasResistance = nil} /// /// Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) public var voltage: Float { - get {return _storage._voltage} + get {return _storage._voltage ?? 0} set {_uniqueStorage()._voltage = newValue} } + /// Returns true if `voltage` has been explicitly set. + public var hasVoltage: Bool {return _storage._voltage != nil} + /// Clears the value of `voltage`. Subsequent reads from it will return its default value. + public mutating func clearVoltage() {_uniqueStorage()._voltage = nil} /// /// Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) public var current: Float { - get {return _storage._current} + get {return _storage._current ?? 0} set {_uniqueStorage()._current = newValue} } + /// Returns true if `current` has been explicitly set. + public var hasCurrent: Bool {return _storage._current != nil} + /// Clears the value of `current`. Subsequent reads from it will return its default value. + public mutating func clearCurrent() {_uniqueStorage()._current = nil} /// /// relative scale IAQ value as measured by Bosch BME680 . value 0-500. /// Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. public var iaq: UInt32 { - get {return _storage._iaq} + get {return _storage._iaq ?? 0} set {_uniqueStorage()._iaq = newValue} } + /// Returns true if `iaq` has been explicitly set. + public var hasIaq: Bool {return _storage._iaq != nil} + /// Clears the value of `iaq`. Subsequent reads from it will return its default value. + public mutating func clearIaq() {_uniqueStorage()._iaq = nil} /// /// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. public var distance: Float { - get {return _storage._distance} + get {return _storage._distance ?? 0} set {_uniqueStorage()._distance = newValue} } + /// Returns true if `distance` has been explicitly set. + public var hasDistance: Bool {return _storage._distance != nil} + /// Clears the value of `distance`. Subsequent reads from it will return its default value. + public mutating func clearDistance() {_uniqueStorage()._distance = nil} /// /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. public var lux: Float { - get {return _storage._lux} + get {return _storage._lux ?? 0} set {_uniqueStorage()._lux = newValue} } + /// Returns true if `lux` has been explicitly set. + public var hasLux: Bool {return _storage._lux != nil} + /// Clears the value of `lux`. Subsequent reads from it will return its default value. + public mutating func clearLux() {_uniqueStorage()._lux = nil} /// /// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. public var whiteLux: Float { - get {return _storage._whiteLux} + get {return _storage._whiteLux ?? 0} set {_uniqueStorage()._whiteLux = newValue} } + /// Returns true if `whiteLux` has been explicitly set. + public var hasWhiteLux: Bool {return _storage._whiteLux != nil} + /// Clears the value of `whiteLux`. Subsequent reads from it will return its default value. + public mutating func clearWhiteLux() {_uniqueStorage()._whiteLux = nil} /// /// Infrared lux public var irLux: Float { - get {return _storage._irLux} + get {return _storage._irLux ?? 0} set {_uniqueStorage()._irLux = newValue} } + /// Returns true if `irLux` has been explicitly set. + public var hasIrLux: Bool {return _storage._irLux != nil} + /// Clears the value of `irLux`. Subsequent reads from it will return its default value. + public mutating func clearIrLux() {_uniqueStorage()._irLux = nil} /// /// Ultraviolet lux public var uvLux: Float { - get {return _storage._uvLux} + get {return _storage._uvLux ?? 0} set {_uniqueStorage()._uvLux = newValue} } + /// Returns true if `uvLux` has been explicitly set. + public var hasUvLux: Bool {return _storage._uvLux != nil} + /// Clears the value of `uvLux`. Subsequent reads from it will return its default value. + public mutating func clearUvLux() {_uniqueStorage()._uvLux = nil} /// /// Wind direction in degrees /// 0 degrees = North, 90 = East, etc... public var windDirection: UInt32 { - get {return _storage._windDirection} + get {return _storage._windDirection ?? 0} set {_uniqueStorage()._windDirection = newValue} } + /// Returns true if `windDirection` has been explicitly set. + public var hasWindDirection: Bool {return _storage._windDirection != nil} + /// Clears the value of `windDirection`. Subsequent reads from it will return its default value. + public mutating func clearWindDirection() {_uniqueStorage()._windDirection = nil} /// /// Wind speed in m/s public var windSpeed: Float { - get {return _storage._windSpeed} + get {return _storage._windSpeed ?? 0} set {_uniqueStorage()._windSpeed = newValue} } + /// Returns true if `windSpeed` has been explicitly set. + public var hasWindSpeed: Bool {return _storage._windSpeed != nil} + /// Clears the value of `windSpeed`. Subsequent reads from it will return its default value. + public mutating func clearWindSpeed() {_uniqueStorage()._windSpeed = nil} /// /// Weight in KG public var weight: Float { - get {return _storage._weight} + get {return _storage._weight ?? 0} set {_uniqueStorage()._weight = newValue} } + /// Returns true if `weight` has been explicitly set. + public var hasWeight: Bool {return _storage._weight != nil} + /// Clears the value of `weight`. Subsequent reads from it will return its default value. + public mutating func clearWeight() {_uniqueStorage()._weight = nil} /// /// Wind gust in m/s public var windGust: Float { - get {return _storage._windGust} + get {return _storage._windGust ?? 0} set {_uniqueStorage()._windGust = newValue} } + /// Returns true if `windGust` has been explicitly set. + public var hasWindGust: Bool {return _storage._windGust != nil} + /// Clears the value of `windGust`. Subsequent reads from it will return its default value. + public mutating func clearWindGust() {_uniqueStorage()._windGust = nil} /// /// Wind lull in m/s public var windLull: Float { - get {return _storage._windLull} + get {return _storage._windLull ?? 0} set {_uniqueStorage()._windLull = newValue} } + /// Returns true if `windLull` has been explicitly set. + public var hasWindLull: Bool {return _storage._windLull != nil} + /// Clears the value of `windLull`. Subsequent reads from it will return its default value. + public mutating func clearWindLull() {_uniqueStorage()._windLull = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -412,31 +521,80 @@ public struct PowerMetrics { /// /// Voltage (Ch1) - public var ch1Voltage: Float = 0 + public var ch1Voltage: Float { + get {return _ch1Voltage ?? 0} + set {_ch1Voltage = newValue} + } + /// Returns true if `ch1Voltage` has been explicitly set. + public var hasCh1Voltage: Bool {return self._ch1Voltage != nil} + /// Clears the value of `ch1Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh1Voltage() {self._ch1Voltage = nil} /// /// Current (Ch1) - public var ch1Current: Float = 0 + public var ch1Current: Float { + get {return _ch1Current ?? 0} + set {_ch1Current = newValue} + } + /// Returns true if `ch1Current` has been explicitly set. + public var hasCh1Current: Bool {return self._ch1Current != nil} + /// Clears the value of `ch1Current`. Subsequent reads from it will return its default value. + public mutating func clearCh1Current() {self._ch1Current = nil} /// /// Voltage (Ch2) - public var ch2Voltage: Float = 0 + public var ch2Voltage: Float { + get {return _ch2Voltage ?? 0} + set {_ch2Voltage = newValue} + } + /// Returns true if `ch2Voltage` has been explicitly set. + public var hasCh2Voltage: Bool {return self._ch2Voltage != nil} + /// Clears the value of `ch2Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh2Voltage() {self._ch2Voltage = nil} /// /// Current (Ch2) - public var ch2Current: Float = 0 + public var ch2Current: Float { + get {return _ch2Current ?? 0} + set {_ch2Current = newValue} + } + /// Returns true if `ch2Current` has been explicitly set. + public var hasCh2Current: Bool {return self._ch2Current != nil} + /// Clears the value of `ch2Current`. Subsequent reads from it will return its default value. + public mutating func clearCh2Current() {self._ch2Current = nil} /// /// Voltage (Ch3) - public var ch3Voltage: Float = 0 + public var ch3Voltage: Float { + get {return _ch3Voltage ?? 0} + set {_ch3Voltage = newValue} + } + /// Returns true if `ch3Voltage` has been explicitly set. + public var hasCh3Voltage: Bool {return self._ch3Voltage != nil} + /// Clears the value of `ch3Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh3Voltage() {self._ch3Voltage = nil} /// /// Current (Ch3) - public var ch3Current: Float = 0 + public var ch3Current: Float { + get {return _ch3Current ?? 0} + set {_ch3Current = newValue} + } + /// Returns true if `ch3Current` has been explicitly set. + public var hasCh3Current: Bool {return self._ch3Current != nil} + /// Clears the value of `ch3Current`. Subsequent reads from it will return its default value. + public mutating func clearCh3Current() {self._ch3Current = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _ch1Voltage: Float? = nil + fileprivate var _ch1Current: Float? = nil + fileprivate var _ch2Voltage: Float? = nil + fileprivate var _ch2Current: Float? = nil + fileprivate var _ch3Voltage: Float? = nil + fileprivate var _ch3Current: Float? = nil } /// @@ -448,55 +606,152 @@ public struct AirQualityMetrics { /// /// Concentration Units Standard PM1.0 - public var pm10Standard: UInt32 = 0 + public var pm10Standard: UInt32 { + get {return _pm10Standard ?? 0} + set {_pm10Standard = newValue} + } + /// Returns true if `pm10Standard` has been explicitly set. + public var hasPm10Standard: Bool {return self._pm10Standard != nil} + /// Clears the value of `pm10Standard`. Subsequent reads from it will return its default value. + public mutating func clearPm10Standard() {self._pm10Standard = nil} /// /// Concentration Units Standard PM2.5 - public var pm25Standard: UInt32 = 0 + public var pm25Standard: UInt32 { + get {return _pm25Standard ?? 0} + set {_pm25Standard = newValue} + } + /// Returns true if `pm25Standard` has been explicitly set. + public var hasPm25Standard: Bool {return self._pm25Standard != nil} + /// Clears the value of `pm25Standard`. Subsequent reads from it will return its default value. + public mutating func clearPm25Standard() {self._pm25Standard = nil} /// /// Concentration Units Standard PM10.0 - public var pm100Standard: UInt32 = 0 + public var pm100Standard: UInt32 { + get {return _pm100Standard ?? 0} + set {_pm100Standard = newValue} + } + /// Returns true if `pm100Standard` has been explicitly set. + public var hasPm100Standard: Bool {return self._pm100Standard != nil} + /// Clears the value of `pm100Standard`. Subsequent reads from it will return its default value. + public mutating func clearPm100Standard() {self._pm100Standard = nil} /// /// Concentration Units Environmental PM1.0 - public var pm10Environmental: UInt32 = 0 + public var pm10Environmental: UInt32 { + get {return _pm10Environmental ?? 0} + set {_pm10Environmental = newValue} + } + /// Returns true if `pm10Environmental` has been explicitly set. + public var hasPm10Environmental: Bool {return self._pm10Environmental != nil} + /// Clears the value of `pm10Environmental`. Subsequent reads from it will return its default value. + public mutating func clearPm10Environmental() {self._pm10Environmental = nil} /// /// Concentration Units Environmental PM2.5 - public var pm25Environmental: UInt32 = 0 + public var pm25Environmental: UInt32 { + get {return _pm25Environmental ?? 0} + set {_pm25Environmental = newValue} + } + /// Returns true if `pm25Environmental` has been explicitly set. + public var hasPm25Environmental: Bool {return self._pm25Environmental != nil} + /// Clears the value of `pm25Environmental`. Subsequent reads from it will return its default value. + public mutating func clearPm25Environmental() {self._pm25Environmental = nil} /// /// Concentration Units Environmental PM10.0 - public var pm100Environmental: UInt32 = 0 + public var pm100Environmental: UInt32 { + get {return _pm100Environmental ?? 0} + set {_pm100Environmental = newValue} + } + /// Returns true if `pm100Environmental` has been explicitly set. + public var hasPm100Environmental: Bool {return self._pm100Environmental != nil} + /// Clears the value of `pm100Environmental`. Subsequent reads from it will return its default value. + public mutating func clearPm100Environmental() {self._pm100Environmental = nil} /// /// 0.3um Particle Count - public var particles03Um: UInt32 = 0 + public var particles03Um: UInt32 { + get {return _particles03Um ?? 0} + set {_particles03Um = newValue} + } + /// Returns true if `particles03Um` has been explicitly set. + public var hasParticles03Um: Bool {return self._particles03Um != nil} + /// Clears the value of `particles03Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles03Um() {self._particles03Um = nil} /// /// 0.5um Particle Count - public var particles05Um: UInt32 = 0 + public var particles05Um: UInt32 { + get {return _particles05Um ?? 0} + set {_particles05Um = newValue} + } + /// Returns true if `particles05Um` has been explicitly set. + public var hasParticles05Um: Bool {return self._particles05Um != nil} + /// Clears the value of `particles05Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles05Um() {self._particles05Um = nil} /// /// 1.0um Particle Count - public var particles10Um: UInt32 = 0 + public var particles10Um: UInt32 { + get {return _particles10Um ?? 0} + set {_particles10Um = newValue} + } + /// Returns true if `particles10Um` has been explicitly set. + public var hasParticles10Um: Bool {return self._particles10Um != nil} + /// Clears the value of `particles10Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles10Um() {self._particles10Um = nil} /// /// 2.5um Particle Count - public var particles25Um: UInt32 = 0 + public var particles25Um: UInt32 { + get {return _particles25Um ?? 0} + set {_particles25Um = newValue} + } + /// Returns true if `particles25Um` has been explicitly set. + public var hasParticles25Um: Bool {return self._particles25Um != nil} + /// Clears the value of `particles25Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles25Um() {self._particles25Um = nil} /// /// 5.0um Particle Count - public var particles50Um: UInt32 = 0 + public var particles50Um: UInt32 { + get {return _particles50Um ?? 0} + set {_particles50Um = newValue} + } + /// Returns true if `particles50Um` has been explicitly set. + public var hasParticles50Um: Bool {return self._particles50Um != nil} + /// Clears the value of `particles50Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles50Um() {self._particles50Um = nil} /// /// 10.0um Particle Count - public var particles100Um: UInt32 = 0 + public var particles100Um: UInt32 { + get {return _particles100Um ?? 0} + set {_particles100Um = newValue} + } + /// Returns true if `particles100Um` has been explicitly set. + public var hasParticles100Um: Bool {return self._particles100Um != nil} + /// Clears the value of `particles100Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles100Um() {self._particles100Um = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _pm10Standard: UInt32? = nil + fileprivate var _pm25Standard: UInt32? = nil + fileprivate var _pm100Standard: UInt32? = nil + fileprivate var _pm10Environmental: UInt32? = nil + fileprivate var _pm25Environmental: UInt32? = nil + fileprivate var _pm100Environmental: UInt32? = nil + fileprivate var _particles03Um: UInt32? = nil + fileprivate var _particles05Um: UInt32? = nil + fileprivate var _particles10Um: UInt32? = nil + fileprivate var _particles25Um: UInt32? = nil + fileprivate var _particles50Um: UInt32? = nil + fileprivate var _particles100Um: UInt32? = nil } /// @@ -681,41 +936,45 @@ extension DeviceMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa // 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.batteryLevel) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.voltage) }() - case 3: try { try decoder.decodeSingularFloatField(value: &self.channelUtilization) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.airUtilTx) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self.uptimeSeconds) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._batteryLevel) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self._voltage) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self._channelUtilization) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self._airUtilTx) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self._uptimeSeconds) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.batteryLevel != 0 { - try visitor.visitSingularUInt32Field(value: self.batteryLevel, fieldNumber: 1) - } - if self.voltage != 0 { - try visitor.visitSingularFloatField(value: self.voltage, fieldNumber: 2) - } - if self.channelUtilization != 0 { - try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 3) - } - if self.airUtilTx != 0 { - try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 4) - } - if self.uptimeSeconds != 0 { - try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 5) - } + // 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 + try { if let v = self._batteryLevel { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 2) + } }() + try { if let v = self._channelUtilization { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try { if let v = self._airUtilTx { + try visitor.visitSingularFloatField(value: v, fieldNumber: 4) + } }() + try { if let v = self._uptimeSeconds { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: DeviceMetrics, rhs: DeviceMetrics) -> Bool { - if lhs.batteryLevel != rhs.batteryLevel {return false} - if lhs.voltage != rhs.voltage {return false} - if lhs.channelUtilization != rhs.channelUtilization {return false} - if lhs.airUtilTx != rhs.airUtilTx {return false} - if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs._batteryLevel != rhs._batteryLevel {return false} + if lhs._voltage != rhs._voltage {return false} + if lhs._channelUtilization != rhs._channelUtilization {return false} + if lhs._airUtilTx != rhs._airUtilTx {return false} + if lhs._uptimeSeconds != rhs._uptimeSeconds {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -744,23 +1003,23 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple ] fileprivate class _StorageClass { - var _temperature: Float = 0 - var _relativeHumidity: Float = 0 - var _barometricPressure: Float = 0 - var _gasResistance: Float = 0 - var _voltage: Float = 0 - var _current: Float = 0 - var _iaq: UInt32 = 0 - var _distance: Float = 0 - var _lux: Float = 0 - var _whiteLux: Float = 0 - var _irLux: Float = 0 - var _uvLux: Float = 0 - var _windDirection: UInt32 = 0 - var _windSpeed: Float = 0 - var _weight: Float = 0 - var _windGust: Float = 0 - var _windLull: Float = 0 + var _temperature: Float? = nil + var _relativeHumidity: Float? = nil + var _barometricPressure: Float? = nil + var _gasResistance: Float? = nil + var _voltage: Float? = nil + var _current: Float? = nil + var _iaq: UInt32? = nil + var _distance: Float? = nil + var _lux: Float? = nil + var _whiteLux: Float? = nil + var _irLux: Float? = nil + var _uvLux: Float? = nil + var _windDirection: UInt32? = nil + var _windSpeed: Float? = nil + var _weight: Float? = nil + var _windGust: Float? = nil + var _windLull: Float? = nil #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -835,57 +1094,61 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._temperature != 0 { - try visitor.visitSingularFloatField(value: _storage._temperature, fieldNumber: 1) - } - if _storage._relativeHumidity != 0 { - try visitor.visitSingularFloatField(value: _storage._relativeHumidity, fieldNumber: 2) - } - if _storage._barometricPressure != 0 { - try visitor.visitSingularFloatField(value: _storage._barometricPressure, fieldNumber: 3) - } - if _storage._gasResistance != 0 { - try visitor.visitSingularFloatField(value: _storage._gasResistance, fieldNumber: 4) - } - if _storage._voltage != 0 { - try visitor.visitSingularFloatField(value: _storage._voltage, fieldNumber: 5) - } - if _storage._current != 0 { - try visitor.visitSingularFloatField(value: _storage._current, fieldNumber: 6) - } - if _storage._iaq != 0 { - try visitor.visitSingularUInt32Field(value: _storage._iaq, fieldNumber: 7) - } - if _storage._distance != 0 { - try visitor.visitSingularFloatField(value: _storage._distance, fieldNumber: 8) - } - if _storage._lux != 0 { - try visitor.visitSingularFloatField(value: _storage._lux, fieldNumber: 9) - } - if _storage._whiteLux != 0 { - try visitor.visitSingularFloatField(value: _storage._whiteLux, fieldNumber: 10) - } - if _storage._irLux != 0 { - try visitor.visitSingularFloatField(value: _storage._irLux, fieldNumber: 11) - } - if _storage._uvLux != 0 { - try visitor.visitSingularFloatField(value: _storage._uvLux, fieldNumber: 12) - } - if _storage._windDirection != 0 { - try visitor.visitSingularUInt32Field(value: _storage._windDirection, fieldNumber: 13) - } - if _storage._windSpeed != 0 { - try visitor.visitSingularFloatField(value: _storage._windSpeed, fieldNumber: 14) - } - if _storage._weight != 0 { - try visitor.visitSingularFloatField(value: _storage._weight, fieldNumber: 15) - } - if _storage._windGust != 0 { - try visitor.visitSingularFloatField(value: _storage._windGust, fieldNumber: 16) - } - if _storage._windLull != 0 { - try visitor.visitSingularFloatField(value: _storage._windLull, fieldNumber: 17) - } + // 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 + try { if let v = _storage._temperature { + try visitor.visitSingularFloatField(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._relativeHumidity { + try visitor.visitSingularFloatField(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._barometricPressure { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try { if let v = _storage._gasResistance { + try visitor.visitSingularFloatField(value: v, fieldNumber: 4) + } }() + try { if let v = _storage._voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 5) + } }() + try { if let v = _storage._current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 6) + } }() + try { if let v = _storage._iaq { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + } }() + try { if let v = _storage._distance { + try visitor.visitSingularFloatField(value: v, fieldNumber: 8) + } }() + try { if let v = _storage._lux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 9) + } }() + try { if let v = _storage._whiteLux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 10) + } }() + try { if let v = _storage._irLux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 11) + } }() + try { if let v = _storage._uvLux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 12) + } }() + try { if let v = _storage._windDirection { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 13) + } }() + try { if let v = _storage._windSpeed { + try visitor.visitSingularFloatField(value: v, fieldNumber: 14) + } }() + try { if let v = _storage._weight { + try visitor.visitSingularFloatField(value: v, fieldNumber: 15) + } }() + try { if let v = _storage._windGust { + try visitor.visitSingularFloatField(value: v, fieldNumber: 16) + } }() + try { if let v = _storage._windLull { + try visitor.visitSingularFloatField(value: v, fieldNumber: 17) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -938,46 +1201,50 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat // 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.decodeSingularFloatField(value: &self.ch1Voltage) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.ch1Current) }() - case 3: try { try decoder.decodeSingularFloatField(value: &self.ch2Voltage) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.ch2Current) }() - case 5: try { try decoder.decodeSingularFloatField(value: &self.ch3Voltage) }() - case 6: try { try decoder.decodeSingularFloatField(value: &self.ch3Current) }() + case 1: try { try decoder.decodeSingularFloatField(value: &self._ch1Voltage) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self._ch1Current) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self._ch2Voltage) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self._ch2Current) }() + case 5: try { try decoder.decodeSingularFloatField(value: &self._ch3Voltage) }() + case 6: try { try decoder.decodeSingularFloatField(value: &self._ch3Current) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.ch1Voltage != 0 { - try visitor.visitSingularFloatField(value: self.ch1Voltage, fieldNumber: 1) - } - if self.ch1Current != 0 { - try visitor.visitSingularFloatField(value: self.ch1Current, fieldNumber: 2) - } - if self.ch2Voltage != 0 { - try visitor.visitSingularFloatField(value: self.ch2Voltage, fieldNumber: 3) - } - if self.ch2Current != 0 { - try visitor.visitSingularFloatField(value: self.ch2Current, fieldNumber: 4) - } - if self.ch3Voltage != 0 { - try visitor.visitSingularFloatField(value: self.ch3Voltage, fieldNumber: 5) - } - if self.ch3Current != 0 { - try visitor.visitSingularFloatField(value: self.ch3Current, fieldNumber: 6) - } + // 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 + try { if let v = self._ch1Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 1) + } }() + try { if let v = self._ch1Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 2) + } }() + try { if let v = self._ch2Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try { if let v = self._ch2Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 4) + } }() + try { if let v = self._ch3Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 5) + } }() + try { if let v = self._ch3Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 6) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: PowerMetrics, rhs: PowerMetrics) -> Bool { - if lhs.ch1Voltage != rhs.ch1Voltage {return false} - if lhs.ch1Current != rhs.ch1Current {return false} - if lhs.ch2Voltage != rhs.ch2Voltage {return false} - if lhs.ch2Current != rhs.ch2Current {return false} - if lhs.ch3Voltage != rhs.ch3Voltage {return false} - if lhs.ch3Current != rhs.ch3Current {return false} + if lhs._ch1Voltage != rhs._ch1Voltage {return false} + if lhs._ch1Current != rhs._ch1Current {return false} + if lhs._ch2Voltage != rhs._ch2Voltage {return false} + if lhs._ch2Current != rhs._ch2Current {return false} + if lhs._ch3Voltage != rhs._ch3Voltage {return false} + if lhs._ch3Current != rhs._ch3Current {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1006,76 +1273,80 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem // 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.pm10Standard) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.pm25Standard) }() - case 3: try { try decoder.decodeSingularUInt32Field(value: &self.pm100Standard) }() - case 4: try { try decoder.decodeSingularUInt32Field(value: &self.pm10Environmental) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self.pm25Environmental) }() - case 6: try { try decoder.decodeSingularUInt32Field(value: &self.pm100Environmental) }() - case 7: try { try decoder.decodeSingularUInt32Field(value: &self.particles03Um) }() - case 8: try { try decoder.decodeSingularUInt32Field(value: &self.particles05Um) }() - case 9: try { try decoder.decodeSingularUInt32Field(value: &self.particles10Um) }() - case 10: try { try decoder.decodeSingularUInt32Field(value: &self.particles25Um) }() - case 11: try { try decoder.decodeSingularUInt32Field(value: &self.particles50Um) }() - case 12: try { try decoder.decodeSingularUInt32Field(value: &self.particles100Um) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._pm10Standard) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self._pm25Standard) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self._pm100Standard) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self._pm10Environmental) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self._pm25Environmental) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self._pm100Environmental) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self._particles03Um) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self._particles05Um) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self._particles10Um) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self._particles25Um) }() + case 11: try { try decoder.decodeSingularUInt32Field(value: &self._particles50Um) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self._particles100Um) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.pm10Standard != 0 { - try visitor.visitSingularUInt32Field(value: self.pm10Standard, fieldNumber: 1) - } - if self.pm25Standard != 0 { - try visitor.visitSingularUInt32Field(value: self.pm25Standard, fieldNumber: 2) - } - if self.pm100Standard != 0 { - try visitor.visitSingularUInt32Field(value: self.pm100Standard, fieldNumber: 3) - } - if self.pm10Environmental != 0 { - try visitor.visitSingularUInt32Field(value: self.pm10Environmental, fieldNumber: 4) - } - if self.pm25Environmental != 0 { - try visitor.visitSingularUInt32Field(value: self.pm25Environmental, fieldNumber: 5) - } - if self.pm100Environmental != 0 { - try visitor.visitSingularUInt32Field(value: self.pm100Environmental, fieldNumber: 6) - } - if self.particles03Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles03Um, fieldNumber: 7) - } - if self.particles05Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles05Um, fieldNumber: 8) - } - if self.particles10Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles10Um, fieldNumber: 9) - } - if self.particles25Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles25Um, fieldNumber: 10) - } - if self.particles50Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles50Um, fieldNumber: 11) - } - if self.particles100Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles100Um, fieldNumber: 12) - } + // 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 + try { if let v = self._pm10Standard { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._pm25Standard { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._pm100Standard { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3) + } }() + try { if let v = self._pm10Environmental { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._pm25Environmental { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) + } }() + try { if let v = self._pm100Environmental { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 6) + } }() + try { if let v = self._particles03Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + } }() + try { if let v = self._particles05Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8) + } }() + try { if let v = self._particles10Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() + try { if let v = self._particles25Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 10) + } }() + try { if let v = self._particles50Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 11) + } }() + try { if let v = self._particles100Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 12) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: AirQualityMetrics, rhs: AirQualityMetrics) -> Bool { - if lhs.pm10Standard != rhs.pm10Standard {return false} - if lhs.pm25Standard != rhs.pm25Standard {return false} - if lhs.pm100Standard != rhs.pm100Standard {return false} - if lhs.pm10Environmental != rhs.pm10Environmental {return false} - if lhs.pm25Environmental != rhs.pm25Environmental {return false} - if lhs.pm100Environmental != rhs.pm100Environmental {return false} - if lhs.particles03Um != rhs.particles03Um {return false} - if lhs.particles05Um != rhs.particles05Um {return false} - if lhs.particles10Um != rhs.particles10Um {return false} - if lhs.particles25Um != rhs.particles25Um {return false} - if lhs.particles50Um != rhs.particles50Um {return false} - if lhs.particles100Um != rhs.particles100Um {return false} + if lhs._pm10Standard != rhs._pm10Standard {return false} + if lhs._pm25Standard != rhs._pm25Standard {return false} + if lhs._pm100Standard != rhs._pm100Standard {return false} + if lhs._pm10Environmental != rhs._pm10Environmental {return false} + if lhs._pm25Environmental != rhs._pm25Environmental {return false} + if lhs._pm100Environmental != rhs._pm100Environmental {return false} + if lhs._particles03Um != rhs._particles03Um {return false} + if lhs._particles05Um != rhs._particles05Um {return false} + if lhs._particles10Um != rhs._particles10Um {return false} + if lhs._particles25Um != rhs._particles25Um {return false} + if lhs._particles50Um != rhs._particles50Um {return false} + if lhs._particles100Um != rhs._particles100Um {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index 81fd9d37..c112ce6e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 81fd9d374f1b364c2812b7a0ca2784a7605b6835 +Subproject commit c112ce6e1392e4bc812655fae5e8461c932b5267 From f6c95fe3280364f4c65c790f29ecd11dd57e403d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 Aug 2024 12:16:14 -0700 Subject: [PATCH 056/333] Add ledheartbeat change event --- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 5 +++++ protobufs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 114898ca..b7f98e36 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -298,6 +298,11 @@ struct DeviceConfig: View { if newTzdef != node!.deviceConfig!.tzdef { hasChanges = true } } } + .onChange(of: ledHeartbeatEnabled) { newLedHeartbeatEnabled in + if node != nil && node?.deviceConfig != nil { + if newLedHeartbeatEnabled != node!.deviceConfig!.ledHeartbeatEnabled { hasChanges = true } + } + } } func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) diff --git a/protobufs b/protobufs index 97674883..2fa7d6a4 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 976748839fafcf0049bb364fe2c7226a194d18a9 +Subproject commit 2fa7d6a4b702fcd58b54b0d1d6e4b3b85164f649 From ee2313ff0d2c44b33de40dbce1bf87eb0af17b30 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 Aug 2024 19:17:05 -0700 Subject: [PATCH 057/333] Throw out any time that is off by more than 10 seconds in either direction and use now for message timestamp --- .../Extensions/CoreData/ChannelEntityExtension.swift | 2 +- Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift | 1 + Meshtastic/Helpers/MeshPackets.swift | 7 ++++++- Meshtastic/Views/ContentView.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 1 - Meshtastic/Views/Settings/Routes.swift | 1 + 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 8f35002f..57babf4a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -14,7 +14,7 @@ extension ChannelEntity { let context = PersistenceController.shared.container.viewContext let fetchRequest = MessageEntity.fetchRequest() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) + fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index 7b07575b..68a48ee1 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -22,6 +22,7 @@ extension MyInfoEntity { let unreadMessages = messageList.filter { ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false } return unreadMessages.count } + var hasAdmin: Bool { let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } return adminChannel?.count ?? 0 > 0 diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d2987e46..4f931c1b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -808,7 +808,12 @@ func textMessageAppPacket( let fetchedUsers = try context.fetch(messageUsers) let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) - newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + /// For message display if the rx time is off by more than 20 seconds set the timestamp to now to assist sorting + if Date().timeIntervalSince1970 < Double(packet.rxTime - 10000) || Date().timeIntervalSince1970 > Double(packet.rxTime + 10000) { + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) + } else { + newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + } newMessage.receivedACK = false newMessage.snr = packet.rxSnr newMessage.rssi = packet.rxRssi diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index b109a318..e8384c35 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -8,7 +8,7 @@ import SwiftUI struct ContentView: View { @ObservedObject var appState: AppState - + @ObservedObject var router: Router diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c442b315..e05cb34b 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -37,7 +37,6 @@ struct NodeList: View { @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 - var boolFilters: [Bool] {[ isOnline, isFavorite, diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index cc70de3b..65be4fd3 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -58,6 +58,7 @@ struct Routes: View { } do { + guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } let routeName = selectedFile.lastPathComponent.dropLast(4) let lines = fileContent.components(separatedBy: "\n") From 9e4def79be049d46d694e9fce174b36b176de109 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 8 Aug 2024 21:52:43 -0700 Subject: [PATCH 058/333] Little tiny locks --- Meshtastic/Views/Messages/MessageContextMenuItems.swift | 2 +- Meshtastic/Views/Messages/UserMessageList.swift | 6 ++++++ protobufs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 7835a723..c9c37cdc 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -56,7 +56,7 @@ struct MessageContextMenuItems: View { let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray) } - + if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 { VStack { Text("SNR \(String(format: "%.2f", message.snr)) dB") diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index ce0eb182..914fde3f 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -57,6 +57,12 @@ struct UserMessageList: View { self.replyMessageId = message.messageId self.messageFieldFocused = true } + if message.pkiEncrypted { + Image(systemName: "lock.circle.fill") + .foregroundStyle(.green) + .frame(height: 25) + .padding(.top, 5) + } if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { RetryButton(message: message, destination: .user(user)) diff --git a/protobufs b/protobufs index c112ce6e..2fa7d6a4 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c112ce6e1392e4bc812655fae5e8461c932b5267 +Subproject commit 2fa7d6a4b702fcd58b54b0d1d6e4b3b85164f649 From c430913015ce3b9ea571fc2e3b377b97d07b7d6f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 9 Aug 2024 21:12:27 -0700 Subject: [PATCH 059/333] Security config updates --- Meshtastic/Helpers/MeshPackets.swift | 7 ++++++- Meshtastic/Persistence/UpdateCoreData.swift | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d2987e46..c210082f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -49,7 +49,7 @@ func generateMessageMarkdown (message: String) -> String { } func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - // We don't care about any of the Power settings, config is available for everything else + if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { @@ -64,6 +64,8 @@ func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int6 upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum, context: context) } } @@ -276,6 +278,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newTelemetries.append(telemetry) newNode.telemetries? = NSOrderedSet(array: newTelemetries) } + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) newNode.snr = nodeInfo.snr @@ -493,6 +496,8 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), context: context) } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { let moduleConfig = adminMessage.getModuleConfigResponse diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 304c6a77..f33527e0 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -769,7 +769,7 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum)) - MeshLogger.log("🌐 \(logString)") + MeshLogger.log("🛡️ \(logString)") let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -799,19 +799,19 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, c do { try context.save() - Logger.data.info("💾 [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") + Logger.data.info("💾 [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { context.rollback() let nsError = error as NSError - Logger.data.error("💥 [NetworkConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + Logger.data.error("💥 [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } } else { - Logger.data.error("💥 [NetworkConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Network Config") + Logger.data.error("💥 [SecurityConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Security Config") } } catch { let nsError = error as NSError - Logger.data.error("💥 [NetworkConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + Logger.data.error("💥 [SecurityConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } } From f87adad009c3b7186a2e73afaf5b8d5b60952af5 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:02:24 -0700 Subject: [PATCH 060/333] Created two App Intents and made a BLEManager singleton --- Meshtastic/AppIntents/MessageChannelIntent.swift | 8 ++++++++ Meshtastic/AppIntents/SendWaypointIntent.swift | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 Meshtastic/AppIntents/MessageChannelIntent.swift create mode 100644 Meshtastic/AppIntents/SendWaypointIntent.swift diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift new file mode 100644 index 00000000..ad7bf1bc --- /dev/null +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -0,0 +1,8 @@ +// +// MessageChannelIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/9/24. +// + +import Foundation diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift new file mode 100644 index 00000000..1eb79f90 --- /dev/null +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -0,0 +1,8 @@ +// +// SendWaypointIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/9/24. +// + +import Foundation From b3f8c9299a850f7f0986a174c7441544b1087e71 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:02:55 -0700 Subject: [PATCH 061/333] Created two App Intents and made a BLEManager singleton --- Localizable.xcstrings | 33 ++++++++ Meshtastic.xcodeproj/project.pbxproj | 16 ++++ .../xcschemes/WidgetsExtension.xcscheme | 1 + .../xcshareddata/swiftpm/Package.resolved | 2 +- .../AppIntents/MessageChannelIntent.swift | 35 +++++++++ .../AppIntents/SendWaypointIntent.swift | 77 +++++++++++++++++++ Meshtastic/Helpers/BLEManager.swift | 37 +++++---- Meshtastic/MeshtasticApp.swift | 19 +++-- 8 files changed, 198 insertions(+), 22 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d80208dd..495575cc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2779,6 +2779,9 @@ }, "Channel Name" : { + }, + "Channel number must be between 0 and 7." : { + }, "Channel Role" : { @@ -5196,6 +5199,9 @@ }, "Description" : { + }, + "Description must be less than 100 bytes" : { + }, "Detection" : { @@ -6808,6 +6814,9 @@ } } } + }, + "Emoji" : { + }, "Empty" : { @@ -7176,6 +7185,9 @@ }, "Factory reset your device and app? " : { + }, + "Failed to encode message content." : { + }, "Failed to get a valid position to exchange" : { @@ -10855,6 +10867,9 @@ }, "Loading Logs. . ." : { + }, + "Location" : { + }, "Location: %@" : { @@ -14500,6 +14515,12 @@ } } } + }, + "Message" : { + + }, + "Message content exceeds 228 bytes." : { + }, "message.details" : { "localizations" : { @@ -15103,6 +15124,9 @@ } } } + }, + "Must be a single emoji" : { + }, "Nag timeout" : { @@ -15167,6 +15191,9 @@ }, "Name" : { + }, + "Name must be less than 30 bytes" : { + }, "Nearby Topics" : { @@ -18995,6 +19022,12 @@ }, "Send" : { + }, + "Send a channel message" : { + + }, + "Send a waypoint" : { + }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cca267e7..0bf7d536 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; + BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; + BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; @@ -262,6 +264,8 @@ 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; + BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; + BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -548,6 +552,15 @@ path = MeshtasticTests; sourceTree = ""; }; + BCB6137F2C6728E700485544 /* AppIntents */ = { + isa = PBXGroup; + children = ( + BCB613802C67290800485544 /* SendWaypointIntent.swift */, + BCB613822C672A2600485544 /* MessageChannelIntent.swift */, + ); + path = AppIntents; + sourceTree = ""; + }; C9483F6B2773016700998F6B /* MapKitMap */ = { isa = PBXGroup; children = ( @@ -814,6 +827,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + BCB6137F2C6728E700485544 /* AppIntents */, DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, DD7709392AA1ABA1007A8BF0 /* Tips */, @@ -1339,6 +1353,7 @@ DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD3CC24C2C498D6C001BD3A2 /* BatteryCompact.swift in Sources */, + BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */, DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, @@ -1375,6 +1390,7 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, + BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme index 880339bc..decd8381 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme @@ -89,6 +89,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index e358787d..c47a9058 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "af29d93455cb8f728684674f544d815b5becb17e049287cc1df8079a4855d0fc", + "originHash" : "1571e0d09fede5d57a2c415019f30868d90fde5a53a863cc277593881c2dc4a5", "pins" : [ { "identity" : "cocoamqtt", diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index ad7bf1bc..a952e5ff 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -6,3 +6,38 @@ // import Foundation +import AppIntents + +struct MessageChannelIntent: AppIntent { + static var title: LocalizedStringResource = "Send a channel message" + + @Parameter(title: "Message") + var messageContent: String + + @Parameter(title: "Channel",controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7)) + var channelNumber: Int + + + static var parameterSummary: some ParameterSummary { + Summary("Send \(\.$messageContent) to \(\.$channelNumber)") + } + func perform() async throws -> some IntentResult { + // Check if channel number is between 1 and 7 + guard (0...7).contains(channelNumber) else { + throw $channelNumber.needsValueError("Channel number must be between 0 and 7.") + } + + // Convert messageContent to data and check its length + guard let messageData = messageContent.data(using: .utf8) else { + throw $messageContent.needsValueError("Failed to encode message content.") + } + + if messageData.count > 228 { + throw $messageContent.needsValueError("Message content exceeds 228 bytes.") + } + if (BLEManager.shared.isConnected){ + BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) + } + return .result() + } +} diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 1eb79f90..43cc93c8 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -5,4 +5,81 @@ // Created by Benjamin Faershtein on 8/9/24. // +import CoreLocation import Foundation +import AppIntents +import MeshtasticProtobufs + +struct SendWaypointIntent: AppIntent { + + static var title = LocalizedStringResource("Send a waypoint") + + @Parameter(title: "Name", default: "Dropped Pin") + var nameParameter: String? + + @Parameter(title: "Description", default: "") + var descriptionParameter: String? + + @Parameter(title: "Emoji", default: "📍") + var emojiParameter: String? + + @Parameter(title: "Location") + var locationParameter: CLPlacemark + + func perform() async throws -> some IntentResult { + + // Provide default values if parameters are nil + let name = nameParameter ?? "Dropped Pin" + let description = descriptionParameter ?? "" + let emoji = emojiParameter ?? "📍" + + // Validate name length + if name.utf8.count > 30 { + throw $nameParameter.needsValueError("Name must be less than 30 bytes") + } + + // Validate description length + if description.utf8.count > 100 { + throw $descriptionParameter.needsValueError("Description must be less than 100 bytes") + } + + // Validate emoji + guard isValidSingleEmoji(emoji) else { + throw $emojiParameter.needsValueError("Must be a single emoji") + } + + var newWaypoint = Waypoint() + + if let latitude = locationParameter.location?.coordinate.latitude { + newWaypoint.latitudeI = Int32(latitude * 10_000_000) + } + + if let longitude = locationParameter.location?.coordinate.longitude { + newWaypoint.longitudeI = Int32(longitude * 10_000_000) + } + + + newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + // This regex pattern is for matching a single emoji + let emojiPattern = "^([\\p{So}\\p{Cn}])$" + let regex = try? NSRegularExpression(pattern: emojiPattern, options: []) + let matches = regex?.matches(in: emoji, options: [], range: NSRange(location: 0, length: emoji.utf16.count)) + + return matches?.count == 1 + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 475235de..4db1197c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -11,6 +11,7 @@ import OSLog // Meshtastic BLE Device Manager // --------------------------------------------------------------------------------------- class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject { + static var shared: BLEManager! // Singleton instance let appState: AppState @@ -54,20 +55,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MARK: init - init( - appState: AppState, - context: NSManagedObjectContext - ) { - self.appState = appState - self.context = context + private override init() { + // Default initialization should not be used + fatalError("Use setup(appState:context:) to initialize the singleton") + } + + static func setup(appState: AppState, context: NSManagedObjectContext) { + guard shared == nil else { + print("BLEManager already initialized") + return + } + shared = BLEManager(appState: appState, context: context) + } + + private init(appState: AppState, context: NSManagedObjectContext) { + self.appState = appState + self.context = context + self.lastConnectionError = "" + self.connectedVersion = "0.0.0" + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + mqttManager.delegate = self + } + - self.lastConnectionError = "" - self.connectedVersion = "0.0.0" - super.init() - centralManager = CBCentralManager(delegate: self, queue: nil) - mqttManager.delegate = self - // centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreKey]) - } // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 4d7dc4c1..c8ba6a05 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -17,8 +17,8 @@ struct MeshtasticAppleApp: App { @ObservedObject var appState: AppState - @ObservedObject - private var bleManager: BLEManager +// @ObservedObject +// private var bleManager: BLEManager private let persistenceController: PersistenceController @@ -35,10 +35,13 @@ struct MeshtasticAppleApp: App { ) self._appState = ObservedObject(wrappedValue: appState) - self.bleManager = BLEManager( - appState: appState, - context: persistenceController.container.viewContext - ) +// self.bleManager = BLEManager( +// appState: appState, +// context: persistenceController.container.viewContext +// ) + + // Initialize the BLEManager singleton with the necessary dependencies + BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) self.persistenceController = persistenceController // Wire up router @@ -53,9 +56,9 @@ struct MeshtasticAppleApp: App { ) .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(appState) - .environmentObject(bleManager) + .environmentObject(BLEManager.shared) .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager) + SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: BLEManager.shared) .presentationDetents([.large]) .presentationDragIndicator(.visible) } From 0d40e5de5dda761f6d57f78bd21d4e7f867cfbd4 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 9 Aug 2024 23:05:23 -0700 Subject: [PATCH 062/333] Cleanup --- Meshtastic/MeshtasticApp.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index c8ba6a05..f926956c 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -35,11 +35,6 @@ struct MeshtasticAppleApp: App { ) self._appState = ObservedObject(wrappedValue: appState) -// self.bleManager = BLEManager( -// appState: appState, -// context: persistenceController.container.viewContext -// ) - // Initialize the BLEManager singleton with the necessary dependencies BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) self.persistenceController = persistenceController From aeb69e14cc0287e32ea87a44bfa26da342f77909 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Aug 2024 07:05:09 -0700 Subject: [PATCH 063/333] simplify bool onchange --- .../Settings/Config/SecurityConfig.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 4c8bd85a..3345c533 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -29,6 +29,14 @@ struct SecurityConfig: View { @State var bluetoothLoggingEnabled = false @State var adminChannelEnabled = false + var boolValues: [Bool] {[ + isManaged, + serialEnabled, + debugLogApiEnabled, + bluetoothLoggingEnabled, + adminChannelEnabled + ]} + var body: some View { VStack { Form { @@ -146,7 +154,41 @@ struct SecurityConfig: View { ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) + .onChange(of: boolValues) { _ in + hasChanges = true + } + SaveConfigButton(node: node, hasChanges: $hasChanges) { + guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), + let fromUser = connectedNode.user, + let toUser = node?.user else { + return + } + + var config = Config.SecurityConfig() + //config.publicKey = publicKey + //config.privateKey = privateKey + //config.adminKey = adminKey + config.isManaged = isManaged + config.serialEnabled = serialEnabled + config.debugLogApiEnabled = debugLogApiEnabled + config.bluetoothLoggingEnabled = bluetoothLoggingEnabled + config.adminChannelEnabled = adminChannelEnabled + + let adminMessageId = bleManager.saveSecurityConfig( + config: config, + fromUser: fromUser, + toUser: toUser, + adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + ) + if adminMessageId > 0 { + // Should show a saved successfully alert once I know that to be true + // for now just disable the button after a successful save + hasChanges = false + goBack() + } + } } + func setSecurityValues() { self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? "" self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" From 56b75cbd3cda09be2579c9863a1dd415ac36f927 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Aug 2024 12:30:33 -0700 Subject: [PATCH 064/333] Match the firmware and do 3 decimal points for decimals --- Meshtastic/Views/Settings/Config/LoRaConfig.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index c98e162c..8239282c 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -49,6 +49,8 @@ struct LoRaConfig: View { let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal + formatter.allowsFloats = true + formatter.maximumFractionDigits = 4 return formatter }() From 9378f5055ad00b8b644d782838337f6a2f6fa71a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Aug 2024 12:48:05 -0700 Subject: [PATCH 065/333] Match conversion --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 4f931c1b..92475a96 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -812,7 +812,7 @@ func textMessageAppPacket( if Date().timeIntervalSince1970 < Double(packet.rxTime - 10000) || Date().timeIntervalSince1970 > Double(packet.rxTime + 10000) { newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) } else { - newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + newMessage.messageTimestamp = Int32(packet.rxTime) } newMessage.receivedACK = false newMessage.snr = packet.rxSnr From 94b96ee219f7bc3cac0c665057e15e4859ed373b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Aug 2024 17:36:50 -0700 Subject: [PATCH 066/333] Set time if it is empty for messages, otherwise leave it alone --- Meshtastic/Helpers/MeshPackets.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 92475a96..cfff9567 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -808,8 +808,7 @@ func textMessageAppPacket( let fetchedUsers = try context.fetch(messageUsers) let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) - /// For message display if the rx time is off by more than 20 seconds set the timestamp to now to assist sorting - if Date().timeIntervalSince1970 < Double(packet.rxTime - 10000) || Date().timeIntervalSince1970 > Double(packet.rxTime + 10000) { + if packet.rxTime == 0 { newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) } else { newMessage.messageTimestamp = Int32(packet.rxTime) From 1b61482c015f8afb2e82fad8d075ae1c349570ac Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Aug 2024 21:20:12 -0700 Subject: [PATCH 067/333] Update protos --- MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift | 8 ++++++++ protobufs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index f526f4ca..3ea4ac4c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -815,6 +815,10 @@ public struct AdminMessage { /// /// TODO: REPLACE case bluetoothConfig // = 6 + + /// + /// TODO: REPLACE + case securityConfig // = 7 case UNRECOGNIZED(Int) public init() { @@ -830,6 +834,7 @@ public struct AdminMessage { case 4: self = .displayConfig case 5: self = .loraConfig case 6: self = .bluetoothConfig + case 7: self = .securityConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -843,6 +848,7 @@ public struct AdminMessage { case .displayConfig: return 4 case .loraConfig: return 5 case .bluetoothConfig: return 6 + case .securityConfig: return 7 case .UNRECOGNIZED(let i): return i } } @@ -966,6 +972,7 @@ extension AdminMessage.ConfigType: CaseIterable { .displayConfig, .loraConfig, .bluetoothConfig, + .securityConfig, ] } @@ -1703,6 +1710,7 @@ extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { 4: .same(proto: "DISPLAY_CONFIG"), 5: .same(proto: "LORA_CONFIG"), 6: .same(proto: "BLUETOOTH_CONFIG"), + 7: .same(proto: "SECURITY_CONFIG"), ] } diff --git a/protobufs b/protobufs index 2fa7d6a4..66eb3184 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 2fa7d6a4b702fcd58b54b0d1d6e4b3b85164f649 +Subproject commit 66eb3184d5651705ae62f80bf4f65b94a62e7f06 From 7b4a3ec648de9dc37bfd5b0bfca2cdae0ee7b47a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 00:08:43 -0700 Subject: [PATCH 068/333] Set security config --- Meshtastic/Persistence/UpdateCoreData.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index f33527e0..e8563315 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -787,6 +787,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, c newSecurityConfig.serialEnabled = config.serialEnabled newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + fetchedNode[0].securityConfig = newSecurityConfig } else { fetchedNode[0].securityConfig?.publicKey = config.publicKey fetchedNode[0].securityConfig?.privateKey = config.privateKey From 3acca7ddcb7776b0b5e0e7e835085dfa9cded193 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 08:09:00 -0700 Subject: [PATCH 069/333] Set saved values --- .../Views/Settings/Config/SecurityConfig.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 3345c533..9be5abd8 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -154,6 +154,18 @@ struct SecurityConfig: View { ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) + .onAppear { + setSecurityValues() + + // Need to request a LoRaConfig from the remote node before allowing changes +// if bleManager.connectedPeripheral != nil && node?.securityConfig == nil { +// Logger.mesh.info("empty security config") +// let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) +// if node != nil && connectedNode != nil { +// _ = bleManager.requestSecurityyConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) +// } +// } + } .onChange(of: boolValues) { _ in hasChanges = true } From c6298f785c2221b4261e0ffd4dfa6096ecb1578a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 09:07:22 -0700 Subject: [PATCH 070/333] Standardize onAppear for settings that support admin messages --- .../Views/Settings/Config/BluetoothConfig.swift | 1 - .../Views/Settings/Config/ConfigHeader.swift | 3 ++- .../Views/Settings/Config/DeviceConfig.swift | 16 ++++++++++------ .../Views/Settings/Config/DisplayConfig.swift | 15 +++++++++------ .../Views/Settings/Config/LoRaConfig.swift | 14 +++++++++----- .../Config/Module/AmbientLightingConfig.swift | 14 +++++++++----- .../Config/Module/CannedMessagesConfig.swift | 14 +++++++++----- .../Config/Module/DetectionSensorConfig.swift | 14 +++++++++----- .../Module/ExternalNotificationConfig.swift | 14 +++++++++----- .../Settings/Config/Module/MQTTConfig.swift | 14 +++++++++----- .../Config/Module/PaxCounterConfig.swift | 1 - .../Settings/Config/Module/RangeTestConfig.swift | 14 +++++++++----- .../Settings/Config/Module/RtttlConfig.swift | 14 +++++++++----- .../Settings/Config/Module/SerialConfig.swift | 15 +++++++++------ .../Config/Module/StoreForwardConfig.swift | 14 +++++++++----- .../Settings/Config/Module/TelemetryConfig.swift | 14 +++++++++----- .../Views/Settings/Config/NetworkConfig.swift | 14 +++++++++----- .../Views/Settings/Config/PositionConfig.swift | 1 - .../Views/Settings/Config/PowerConfig.swift | 3 --- .../Views/Settings/Config/SecurityConfig.swift | 12 ------------ 20 files changed, 129 insertions(+), 92 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index b43813c2..aa36e193 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -107,7 +107,6 @@ struct BluetoothConfig: View { } ) .onAppear { - setBluetoothValues() // Need to request a BluetoothConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node, node.bluetoothConfig == nil { Logger.mesh.info("empty bluetooth config") diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index 3ff815f8..cef7f98c 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -23,11 +23,12 @@ struct ConfigHeader: View { .foregroundColor(.orange) } else { Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") - .font(.title3) .onAppear(perform: onAppear) + .font(.title3) } } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 { Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .onAppear(perform: onAppear) } else { Text("Please connect to a radio to configure settings.") .font(.callout) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 114898ca..0d3349e1 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -233,13 +233,17 @@ struct DeviceConfig: View { Spacer() } .navigationTitle("device.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setDeviceValues() - // Need to request a LoRaConfig from the remote node before allowing changes + // Need to request a DeviceConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil { Logger.mesh.info("empty device config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index f800ebb6..0b497037 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -154,13 +154,16 @@ struct DisplayConfig: View { } .navigationTitle("display.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setDisplayValues() - // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.displayConfig == nil { Logger.mesh.info("empty display config") diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index c98e162c..d2062468 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -221,12 +221,16 @@ struct LoRaConfig: View { } } .navigationTitle("lora.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setLoRaValues() // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil { Logger.mesh.info("empty lora config") diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 3fe5560d..1949db37 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -80,12 +80,16 @@ struct AmbientLightingConfig: View { } } .navigationTitle("ambient.lighting.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setAmbientLightingConfigValue() // Need to request a Ambient Lighting Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 670e24e1..d30872d5 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -224,12 +224,16 @@ struct CannedMessagesConfig: View { } } .navigationTitle("canned.messages.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setCannedMessagesValues() // Need to request a CannedMessagesModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.cannedMessageConfig == nil { Logger.mesh.info("empty canned messages module config") diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 480c4e24..358576d4 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -180,12 +180,16 @@ struct DetectionSensorConfig: View { } } .navigationTitle("detection.sensor.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setDetectionSensorValues() // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { Logger.mesh.info("empty detection sensor module config") diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index abdca8a2..fc6f33f2 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -190,12 +190,16 @@ struct ExternalNotificationConfig: View { } } .navigationTitle("external.notification.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setExternalNotificationValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.externalNotificationConfig == nil { Logger.mesh.info("empty external notification module config") diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index f923e560..91bb80e1 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -271,10 +271,15 @@ struct MQTTConfig: View { } } .navigationTitle("mqtt.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected) - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { if newAddress != node!.mqttConfig!.address { hasChanges = true } @@ -357,7 +362,6 @@ struct MQTTConfig: View { } } .onAppear { - setMqttValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { Logger.mesh.info("empty mqtt module config") diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index d7670a7c..ce9d34f8 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -58,7 +58,6 @@ struct PaxCounterConfig: View { ) }) .onAppear { - setPaxValues() // Need to request a PAX Counter module config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.paxCounterConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 0bf99d65..51c847e6 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -72,12 +72,16 @@ struct RangeTestConfig: View { } } .navigationTitle("range.test.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setRangeTestValues() // Need to request a RangeTestModule Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { Logger.mesh.debug("empty range test module config") diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 3128fffe..95f237a3 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -62,12 +62,16 @@ struct RtttlConfig: View { } } .navigationTitle("config.ringtone.title") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setRtttLConfigValue() // Need to request a Rtttl Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 813e1328..6d5b4421 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -127,13 +127,16 @@ struct SerialConfig: View { } } .navigationTitle("serial.config") - .navigationBarItems(trailing: - - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setSerialValues() // Need to request a SerialModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.serialConfig == nil { Logger.mesh.debug("empty serial module config") diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index e73380f3..4829a5fa 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -137,10 +137,15 @@ struct StoreForwardConfig: View { } } .navigationTitle("storeforward.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil { @@ -150,7 +155,6 @@ struct StoreForwardConfig: View { _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } - setStoreAndForwardValues() } .onChange(of: enabled) { newEnabled in if node != nil && node?.storeForwardConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index a1f827b7..df811677 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -125,12 +125,16 @@ struct TelemetryConfig: View { } } .navigationTitle("telemetry.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setTelemetryValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil { Logger.mesh.info("empty telemetry module config") diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index d969eab9..3bc28ef6 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -109,12 +109,16 @@ struct NetworkConfig: View { } } .navigationTitle("network.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setNetworkValues() // Need to request a NetworkConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.networkConfig == nil { Logger.mesh.info("empty network config") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index aa6960a0..2fd9bcf1 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -377,7 +377,6 @@ struct PositionConfig: View { } ) .onAppear { - setPositionValues() supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame // Need to request a PositionConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, node?.positionConfig == nil { diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index d8de7e44..8f67d6bd 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -119,7 +119,6 @@ struct PowerConfig: View { } .onAppear { Api().loadDeviceHardwareData { (hw) in - for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") @@ -128,8 +127,6 @@ struct PowerConfig: View { } } } - setPowerValues() - // Need to request a Power config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.powerConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 9be5abd8..3345c533 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -154,18 +154,6 @@ struct SecurityConfig: View { ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) - .onAppear { - setSecurityValues() - - // Need to request a LoRaConfig from the remote node before allowing changes -// if bleManager.connectedPeripheral != nil && node?.securityConfig == nil { -// Logger.mesh.info("empty security config") -// let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) -// if node != nil && connectedNode != nil { -// _ = bleManager.requestSecurityyConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) -// } -// } - } .onChange(of: boolValues) { _ in hasChanges = true } From cbfde743afd9975f6ba8b1b95d89835e211d0881 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 09:59:52 -0700 Subject: [PATCH 071/333] Clean up save button display --- .../Settings/Config/SecurityConfig.swift | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 3345c533..1cc0fb7b 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -29,14 +29,6 @@ struct SecurityConfig: View { @State var bluetoothLoggingEnabled = false @State var adminChannelEnabled = false - var boolValues: [Bool] {[ - isManaged, - serialEnabled, - debugLogApiEnabled, - bluetoothLoggingEnabled, - adminChannelEnabled - ]} - var body: some View { VStack { Form { @@ -150,13 +142,39 @@ struct SecurityConfig: View { } } .navigationTitle("Security Config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + .navigationBarItems(trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" + ) }) - .onChange(of: boolValues) { _ in - hasChanges = true + .onChange(of: isManaged) { newIsManaged in + if node != nil && node!.securityConfig != nil { + if newIsManaged != node!.securityConfig!.isManaged { hasChanges = true } + } } + .onChange(of: serialEnabled) { newSerialEnabled in + if node != nil && node!.securityConfig != nil { + if newSerialEnabled != node!.securityConfig!.serialEnabled { hasChanges = true } + } + } + .onChange(of: debugLogApiEnabled) { newDebugLogApiEnabled in + if node != nil && node!.securityConfig != nil { + if newDebugLogApiEnabled != node!.securityConfig!.debugLogApiEnabled { hasChanges = true } + } + } + .onChange(of: bluetoothLoggingEnabled) { newBluetoothLoggingEnabled in + if node != nil && node!.securityConfig != nil { + if newBluetoothLoggingEnabled != node!.securityConfig!.bluetoothLoggingEnabled { hasChanges = true } + } + } + .onChange(of: adminChannelEnabled) { newAdminChannelEnabled in + if node != nil && node!.securityConfig != nil { + if newAdminChannelEnabled != node!.securityConfig!.adminChannelEnabled { hasChanges = true } + } + } + SaveConfigButton(node: node, hasChanges: $hasChanges) { guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), let fromUser = connectedNode.user, From 3786bf5e1fa917cd3963042c553cb9cbc435d24e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 10:04:17 -0700 Subject: [PATCH 072/333] New plan --- .../Settings/Config/SecurityConfig.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 1cc0fb7b..a3aa1f45 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -149,30 +149,30 @@ struct SecurityConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) - .onChange(of: isManaged) { newIsManaged in - if node != nil && node!.securityConfig != nil { - if newIsManaged != node!.securityConfig!.isManaged { hasChanges = true } + .onChange(of: isManaged) { + if let val = node?.securityConfig?.isManaged { + hasChanges = $0 != val } } - .onChange(of: serialEnabled) { newSerialEnabled in - if node != nil && node!.securityConfig != nil { - if newSerialEnabled != node!.securityConfig!.serialEnabled { hasChanges = true } + .onChange(of: serialEnabled) { + if let val = node?.securityConfig?.serialEnabled { + hasChanges = $0 != val + } + } + .onChange(of: debugLogApiEnabled) { + if let val = node?.securityConfig?.debugLogApiEnabled { + hasChanges = $0 != val } } - .onChange(of: debugLogApiEnabled) { newDebugLogApiEnabled in - if node != nil && node!.securityConfig != nil { - if newDebugLogApiEnabled != node!.securityConfig!.debugLogApiEnabled { hasChanges = true } + .onChange(of: bluetoothLoggingEnabled) { + if let val = node?.securityConfig?.bluetoothLoggingEnabled { + hasChanges = $0 != val } } - .onChange(of: bluetoothLoggingEnabled) { newBluetoothLoggingEnabled in - if node != nil && node!.securityConfig != nil { - if newBluetoothLoggingEnabled != node!.securityConfig!.bluetoothLoggingEnabled { hasChanges = true } - } - } - .onChange(of: adminChannelEnabled) { newAdminChannelEnabled in - if node != nil && node!.securityConfig != nil { - if newAdminChannelEnabled != node!.securityConfig!.adminChannelEnabled { hasChanges = true } - } + .onChange(of: adminChannelEnabled) { + if let val = node?.securityConfig?.adminChannelEnabled { + hasChanges = $0 != val + } } SaveConfigButton(node: node, hasChanges: $hasChanges) { From 6f0fe8fd6fb0d0464cf3947a33d4bf2436b909ad Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 17:31:27 -0700 Subject: [PATCH 073/333] Clean up change events --- Meshtastic/Helpers/BLEManager.swift | 5 ++ .../Settings/Config/BluetoothConfig.swift | 20 +++---- .../Views/Settings/Config/DeviceConfig.swift | 34 ++++------- .../Views/Settings/Config/DisplayConfig.swift | 18 ++---- .../Views/Settings/Config/LoRaConfig.swift | 28 +++------ .../Config/Module/AmbientLightingConfig.swift | 20 ++++--- .../Config/Module/CannedMessagesConfig.swift | 24 ++++---- .../Config/Module/DetectionSensorConfig.swift | 18 +++--- .../Module/ExternalNotificationConfig.swift | 60 +++++++++---------- .../Settings/Config/Module/MQTTConfig.swift | 53 ++++++---------- .../Config/Module/PaxCounterConfig.swift | 8 +-- .../Config/Module/RangeTestConfig.swift | 18 ++---- .../Settings/Config/Module/SerialConfig.swift | 29 ++------- .../Views/Settings/Config/NetworkConfig.swift | 18 ++---- .../Settings/Config/PositionConfig.swift | 52 +++++++--------- .../Views/Settings/Config/PowerConfig.swift | 16 +++-- 16 files changed, 167 insertions(+), 254 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ec03b2f8..fdbd2d52 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1140,6 +1140,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate guard let lastLocation = LocationsHandler.shared.locationsArray.last else { return nil } + + if lastLocation == CLLocation(latitude: 0, longitude: 0) { + return nil + } + positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) let timestamp = lastLocation.timestamp diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index aa36e193..2e12dbdb 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -116,25 +116,19 @@ struct BluetoothConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.bluetoothConfig != nil { - if newEnabled != node!.bluetoothConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { + if $0 != node?.bluetoothConfig?.enabled { hasChanges = true } } - .onChange(of: mode) { newMode in - if node != nil && node!.bluetoothConfig != nil { - if newMode != node!.bluetoothConfig!.mode { hasChanges = true } - } + .onChange(of: mode) { + if $0 != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true } } .onChange(of: fixedPin) { newFixedPin in - if node != nil && node!.bluetoothConfig != nil { + if node != nil && node?.bluetoothConfig != nil { if newFixedPin != String(node!.bluetoothConfig!.fixedPin) { hasChanges = true } } } - .onChange(of: deviceLoggingEnabled) { newDeviceLogging in - if node != nil && node!.bluetoothConfig != nil { - if newDeviceLogging != node!.bluetoothConfig!.deviceLoggingEnabled { hasChanges = true } - } + .onChange(of: deviceLoggingEnabled) { + if $0 != node?.bluetoothConfig?.deviceLoggingEnabled { hasChanges = true } } } func setBluetoothValues() { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 0d3349e1..bde4d2ff 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -155,7 +155,7 @@ struct DeviceConfig: View { .disabled(node?.user == nil) .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.leading) .confirmationDialog( "are.you.sure", @@ -180,7 +180,7 @@ struct DeviceConfig: View { .disabled(node?.user == nil) .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.trailing) .confirmationDialog( "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth.", @@ -252,20 +252,14 @@ struct DeviceConfig: View { } } } - .onChange(of: deviceRole) { newRole in - if node != nil && node?.deviceConfig != nil { - if newRole != node!.deviceConfig!.role { hasChanges = true } - } + .onChange(of: deviceRole) { + if $0 != node?.deviceConfig?.role ?? -1 { hasChanges = true } } - .onChange(of: serialEnabled) { newSerial in - if node != nil && node?.deviceConfig != nil { - if newSerial != node!.deviceConfig!.serialEnabled { hasChanges = true } - } + .onChange(of: serialEnabled) { + if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true } } - .onChange(of: debugLogEnabled) { newDebugLog in - if node != nil && node?.deviceConfig != nil { - if newDebugLog != node!.deviceConfig!.debugLogEnabled { hasChanges = true } - } + .onChange(of: debugLogEnabled) { + if $0 != node?.deviceConfig?.debugLogEnabled { hasChanges = true } } .onChange(of: buttonGPIO) { newButtonGPIO in if node != nil && node?.deviceConfig != nil { @@ -287,15 +281,11 @@ struct DeviceConfig: View { if newNodeInfoBroadcastSecs != node!.deviceConfig!.nodeInfoBroadcastSecs { hasChanges = true } } } - .onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in - if node != nil && node?.deviceConfig != nil { - if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true } - } + .onChange(of: doubleTapAsButtonPress) { + if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } } - .onChange(of: isManaged) { newIsManaged in - if node != nil && node?.deviceConfig != nil { - if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true } - } + .onChange(of: isManaged) { + if $0 != node?.deviceConfig?.isManaged { hasChanges = true } } .onChange(of: tzdef) { newTzdef in if node != nil && node?.deviceConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 0b497037..c26d741b 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -183,25 +183,19 @@ struct DisplayConfig: View { if newCarouselSecs != node!.displayConfig!.screenCarouselInterval { hasChanges = true } } } - .onChange(of: compassNorthTop) { newCompassNorthTop in - if node != nil && node!.displayConfig != nil { - if newCompassNorthTop != node!.displayConfig!.compassNorthTop { hasChanges = true } - } + .onChange(of: compassNorthTop) { + if $0 != node?.displayConfig?.compassNorthTop { hasChanges = true } } - .onChange(of: wakeOnTapOrMotion) { newWakeOnTapOrMotion in - if node != nil && node!.displayConfig != nil { - if newWakeOnTapOrMotion != node!.displayConfig!.wakeOnTapOrMotion { hasChanges = true } - } + .onChange(of: wakeOnTapOrMotion) { + if $0 != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true } } .onChange(of: gpsFormat) { newGpsFormat in if node != nil && node!.displayConfig != nil { if newGpsFormat != node!.displayConfig!.gpsFormat { hasChanges = true } } } - .onChange(of: flipScreen) { newFlipScreen in - if node != nil && node!.displayConfig != nil { - if newFlipScreen != node!.displayConfig!.flipScreen { hasChanges = true } - } + .onChange(of: flipScreen) { + if $0 != node?.displayConfig?.flipScreen { hasChanges = true } } .onChange(of: oledType) { newOledType in if node != nil && node!.displayConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index d2062468..24a79c31 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -245,10 +245,8 @@ struct LoRaConfig: View { if newRegion != node!.loRaConfig!.regionCode { hasChanges = true } } } - .onChange(of: usePreset) { newUsePreset in - if node != nil && node!.loRaConfig != nil { - if newUsePreset != node!.loRaConfig!.usePreset { hasChanges = true } - } + .onChange(of: usePreset) { + if $0 != node?.loRaConfig?.usePreset { hasChanges = true } } .onChange(of: modemPreset) { newModemPreset in if node != nil && node!.loRaConfig != nil { @@ -280,30 +278,22 @@ struct LoRaConfig: View { if newSpreadFactor != node!.loRaConfig!.spreadFactor { hasChanges = true } } } - .onChange(of: rxBoostedGain) { newRxBoostedGain in - if node != nil && node!.loRaConfig != nil { - if newRxBoostedGain != node!.loRaConfig!.sx126xRxBoostedGain { hasChanges = true } - } + .onChange(of: rxBoostedGain) { + if $0 != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true } } .onChange(of: overrideFrequency) { newOverrideFrequency in - if node != nil && node!.loRaConfig != nil { - if newOverrideFrequency != node!.loRaConfig!.overrideFrequency { hasChanges = true } - } + if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true } } .onChange(of: txPower) { newTxPower in if node != nil && node!.loRaConfig != nil { if newTxPower != node!.loRaConfig!.txPower { hasChanges = true } } } - .onChange(of: txEnabled) { newTxEnabled in - if node != nil && node!.loRaConfig != nil { - if newTxEnabled != node!.loRaConfig!.txEnabled { hasChanges = true } - } + .onChange(of: txEnabled) { + if $0 != node?.loRaConfig?.txEnabled { hasChanges = true } } - .onChange(of: ignoreMqtt) { newIgnoreMqtt in - if node != nil && node!.loRaConfig != nil { - if newIgnoreMqtt != node!.loRaConfig!.ignoreMqtt { hasChanges = true } - } + .onChange(of: ignoreMqtt) { + if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true } } } func setLoRaValues() { diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 1949db37..76c98752 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -50,10 +50,6 @@ struct AmbientLightingConfig: View { Stepper("Current: \(current)", value: $current, in: 0...31, step: 1) .padding(5) } - .onChange(of: color, initial: true) { - components = color.resolve(in: environment) - hasChanges = true - } } } .disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil) @@ -98,9 +94,19 @@ struct AmbientLightingConfig: View { } } } - .onChange(of: ledState) { newLedState in - if node != nil && node!.ambientLightingConfig != nil { - if newLedState != node!.ambientLightingConfig!.ledState { hasChanges = true } + .onChange(of: ledState) { + if let val = node?.ambientLightingConfig?.ledState { + hasChanges = $0 != val + } + } + .onChange(of: current) { + if let val = node?.ambientLightingConfig?.current { + hasChanges = $0 != val + } + } + .onChange(of: color) { c in + if color != c { + hasChanges = true } } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index d30872d5..b4ec0b63 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -272,24 +272,24 @@ struct CannedMessagesConfig: View { hasChanges = true } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.cannedMessageConfig != nil { - if newEnabled != node!.cannedMessageConfig!.enabled { hasChanges = true } + .onChange(of: enabled) { + if let val = node?.cannedMessageConfig?.enabled { + hasChanges = $0 != val } } - .onChange(of: sendBell) { newBell in - if node != nil && node!.cannedMessageConfig != nil { - if newBell != node!.cannedMessageConfig!.sendBell { hasChanges = true } + .onChange(of: sendBell) { + if let val = node?.cannedMessageConfig?.sendBell { + hasChanges = $0 != val } } - .onChange(of: rotary1Enabled) { newRot1 in - if node != nil && node!.cannedMessageConfig != nil { - if newRot1 != node!.cannedMessageConfig!.rotary1Enabled { hasChanges = true } + .onChange(of: rotary1Enabled) { + if let val = node?.cannedMessageConfig?.rotary1Enabled { + hasChanges = $0 != val } } - .onChange(of: updown1Enabled) { newUpDown in - if node != nil && node!.cannedMessageConfig != nil { - if newUpDown != node!.cannedMessageConfig!.updown1Enabled { hasChanges = true } + .onChange(of: updown1Enabled) { + if let val = node?.cannedMessageConfig?.updown1Enabled { + hasChanges = $0 != val } } .onChange(of: inputbrokerPinA) { newPinA in diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 358576d4..1ff1ed86 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -199,14 +199,14 @@ struct DetectionSensorConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node?.detectionSensorConfig != nil { - if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true } + .onChange(of: enabled) { + if let val = node?.detectionSensorConfig?.enabled { + hasChanges = $0 != val } } - .onChange(of: sendBell) { newSendBell in - if node != nil && node?.detectionSensorConfig != nil { - if newSendBell != node!.detectionSensorConfig!.sendBell { hasChanges = true } + .onChange(of: sendBell) { + if let val = node?.detectionSensorConfig?.sendBell { + hasChanges = $0 != val } } .onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in @@ -214,9 +214,9 @@ struct DetectionSensorConfig: View { if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true } } } - .onChange(of: usePullup) { newUsePullup in - if node != nil && node?.detectionSensorConfig != nil { - if newUsePullup != node!.detectionSensorConfig!.usePullup { hasChanges = true } + .onChange(of: usePullup) { + if let val = node?.detectionSensorConfig?.usePullup { + hasChanges = $0 != val } } .onChange(of: name) { newName in diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index fc6f33f2..32cacbd3 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -209,44 +209,44 @@ struct ExternalNotificationConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.externalNotificationConfig != nil { - if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } + .onChange(of: enabled) { + if let val = node?.externalNotificationConfig?.enabled { + hasChanges = $0 != val } } - .onChange(of: alertBell) { newAlertBell in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } + .onChange(of: alertBell) { + if let val = node?.externalNotificationConfig?.alertBell { + hasChanges = $0 != val } } - .onChange(of: alertBellBuzzer) { newAlertBellBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBellBuzzer != node!.externalNotificationConfig!.alertBellBuzzer { hasChanges = true } + .onChange(of: alertBellBuzzer) { + if let val = node?.externalNotificationConfig?.alertBellBuzzer { + hasChanges = $0 != val } } - .onChange(of: alertBellVibra) { newAlertBellVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBellVibra != node!.externalNotificationConfig!.alertBellVibra { hasChanges = true } + .onChange(of: alertBellVibra) { + if let val = node?.externalNotificationConfig?.alertBellVibra { + hasChanges = $0 != val } } - .onChange(of: alertMessage) { newAlertMessage in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } + .onChange(of: alertMessage) { + if let val = node?.externalNotificationConfig?.alertMessage { + hasChanges = $0 != val } } - .onChange(of: alertMessageBuzzer) { newAlertMessageBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessageBuzzer != node!.externalNotificationConfig!.alertMessageBuzzer { hasChanges = true } + .onChange(of: alertMessageBuzzer) { + if let val = node?.externalNotificationConfig?.alertMessageBuzzer { + hasChanges = $0 != val } } - .onChange(of: alertMessageVibra) { newAlertMessageVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessageVibra != node!.externalNotificationConfig!.alertMessageVibra { hasChanges = true } + .onChange(of: alertMessageVibra) { + if let val = node?.externalNotificationConfig?.alertMessageVibra { + hasChanges = $0 != val } } - .onChange(of: active) { newActive in - if node != nil && node!.externalNotificationConfig != nil { - if newActive != node!.externalNotificationConfig!.active { hasChanges = true } + .onChange(of: active) { + if let val = node?.externalNotificationConfig?.active { + hasChanges = $0 != val } } .onChange(of: output) { newOutput in @@ -269,9 +269,9 @@ struct ExternalNotificationConfig: View { if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } } } - .onChange(of: usePWM) { newUsePWM in - if node != nil && node!.externalNotificationConfig != nil { - if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } + .onChange(of: usePWM) { + if let val = node?.externalNotificationConfig?.usePWM { + hasChanges = $0 != val } } .onChange(of: nagTimeout) { newNagTimeout in @@ -279,9 +279,9 @@ struct ExternalNotificationConfig: View { if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } } } - .onChange(of: useI2SAsBuzzer) { newUseI2SAsBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newUseI2SAsBuzzer != node!.externalNotificationConfig!.useI2SAsBuzzer { hasChanges = true } + .onChange(of: useI2SAsBuzzer) { + if let val = node?.externalNotificationConfig?.useI2SAsBuzzer { + hasChanges = $0 != val } } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 91bb80e1..d6176526 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -280,6 +280,18 @@ struct MQTTConfig: View { ) } ) + .onChange(of: enabled) { + if $0 != node?.mqttConfig?.enabled { hasChanges = true } + } + .onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in + if newProxyToClientEnabled { + jsonEnabled = false + } + if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true } + if newProxyToClientEnabled { + jsonEnabled = false + } + } .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { if newAddress != node!.mqttConfig!.address { hasChanges = true } @@ -303,39 +315,17 @@ struct MQTTConfig: View { .onChange(of: selectedTopic) { newSelectedTopic in root = newSelectedTopic } - .onChange(of: enabled) { newEnabled in - if node != nil && node?.mqttConfig != nil { - if newEnabled != node!.mqttConfig!.enabled { hasChanges = true } - } - } - .onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in - if newProxyToClientEnabled { - jsonEnabled = false - } - if node != nil && node?.mqttConfig != nil { - if newProxyToClientEnabled != node!.mqttConfig!.proxyToClientEnabled { hasChanges = true } - if newProxyToClientEnabled { - jsonEnabled = false - } - } - } - .onChange(of: encryptionEnabled) { newEncryptionEnabled in - if node != nil && node?.mqttConfig != nil { - if newEncryptionEnabled != node!.mqttConfig!.encryptionEnabled { hasChanges = true } - } + .onChange(of: encryptionEnabled) { + if $0 != node?.mqttConfig?.encryptionEnabled { hasChanges = true } } .onChange(of: jsonEnabled) { newJsonEnabled in if newJsonEnabled { proxyToClientEnabled = false } - if node != nil && node?.mqttConfig != nil { - if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true } - } + if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true } } - .onChange(of: tlsEnabled) { newTlsEnabled in - if node != nil && node?.mqttConfig != nil { - if newTlsEnabled != node!.mqttConfig!.tlsEnabled { hasChanges = true } - } + .onChange(of: tlsEnabled) { + if $0 != node?.mqttConfig?.tlsEnabled { hasChanges = true } } .onChange(of: mqttConnected) { newMqttConnected in if newMqttConnected == false { @@ -348,13 +338,8 @@ struct MQTTConfig: View { } } } - .onChange(of: mapReportingEnabled) { newMapReportingEnabled in - if node != nil && node?.mqttConfig != nil { - if newMapReportingEnabled != node!.mqttConfig!.mapReportingEnabled { hasChanges = true } - } - } - .onChange(of: preciseLocation) { _ in - hasChanges = true + .onChange(of: mapReportingEnabled) { + if $0 != node?.mqttConfig?.mapReportingEnabled { hasChanges = true } } .onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in if node != nil && node?.mqttConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index ce9d34f8..6e7bef08 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -67,14 +67,10 @@ struct PaxCounterConfig: View { } } .onChange(of: enabled) { - if let val = node?.paxCounterConfig?.enabled { - hasChanges = $0 != val - } + if $0 != node?.paxCounterConfig?.enabled { hasChanges = true } } .onChange(of: paxcounterUpdateInterval) { - if let val = node?.paxCounterConfig?.updateInterval { - hasChanges = $0 != val - } + if $0 != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 51c847e6..001ad2b1 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -91,20 +91,14 @@ struct RangeTestConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.rangeTestConfig != nil { - if newEnabled != node!.rangeTestConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { + if $0 != node?.rangeTestConfig?.enabled { hasChanges = true } } - .onChange(of: save) { newSave in - if node != nil && node!.rangeTestConfig != nil { - if newSave != node!.rangeTestConfig!.save { hasChanges = true } - } + .onChange(of: save) { + if $0 != node?.rangeTestConfig?.save { hasChanges = true } } - .onChange(of: sender) { newSender in - if node != nil && node!.rangeTestConfig != nil { - if newSender != node!.rangeTestConfig!.sender { hasChanges = true } - } + .onChange(of: sender) { + if $0 != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 6d5b4421..c7243a87 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -145,61 +145,40 @@ struct SerialConfig: View { _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } - } - .onChange(of: enabled) { newEnabled in - - if node != nil && node!.serialConfig != nil { - - if newEnabled != node!.serialConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { + if $0 != node?.serialConfig?.enabled { hasChanges = true } } - .onChange(of: echo) { newEcho in - - if node != nil && node!.serialConfig != nil { - - if newEcho != node!.serialConfig!.echo { hasChanges = true } - } + .onChange(of: echo) { + if $0 != node?.serialConfig?.echo { hasChanges = true } } .onChange(of: rxd) { newRxd in - if node != nil && node!.serialConfig != nil { - if newRxd != node!.serialConfig!.rxd { hasChanges = true } } } .onChange(of: txd) { newTxd in - if node != nil && node!.serialConfig != nil { - if newTxd != node!.serialConfig!.txd { hasChanges = true } } } .onChange(of: baudRate) { newBaud in - if node != nil && node!.serialConfig != nil { - if newBaud != node!.serialConfig!.baudRate { hasChanges = true } } } .onChange(of: timeout) { newTimeout in - if node != nil && node!.serialConfig != nil { - if newTimeout != node!.serialConfig!.timeout { hasChanges = true } } } .onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in - if node != nil && node!.serialConfig != nil { - if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true } } } .onChange(of: mode) { newMode in - if node != nil && node!.serialConfig != nil { - if newMode != node!.serialConfig!.mode { hasChanges = true } } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 3bc28ef6..fb260a69 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -128,10 +128,8 @@ struct NetworkConfig: View { } } } - .onChange(of: wifiEnabled) { newEnabled in - if node != nil && node!.networkConfig != nil { - if newEnabled != node!.networkConfig!.wifiEnabled { hasChanges = true } - } + .onChange(of: wifiEnabled) { + if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true } } .onChange(of: wifiSsid) { newSSID in if node != nil && node!.networkConfig != nil { @@ -143,15 +141,11 @@ struct NetworkConfig: View { if newPsk != node!.networkConfig!.wifiPsk { hasChanges = true } } } - .onChange(of: wifiMode) { newMode in - if node != nil && node!.networkConfig != nil { - if newMode != node!.networkConfig!.wifiMode { hasChanges = true } - } + .onChange(of: wifiMode) { + if $0 != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true } } - .onChange(of: ethEnabled) { newEthEnabled in - if node != nil && node!.networkConfig != nil { - if newEthEnabled != node!.networkConfig!.ethEnabled { hasChanges = true } - } + .onChange(of: ethEnabled) { + if $0 != node?.networkConfig?.ethEnabled { hasChanges = true } } } func setNetworkValues() { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 2fd9bcf1..0f71b379 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -404,51 +404,39 @@ struct PositionConfig: View { } } } - .onChange(of: gpsMode) { _ in - handleChanges() + .onChange(of: gpsMode) { newGpsMode in + if newGpsMode != node?.positionConfig?.gpsMode ?? 0 { hasChanges = true } } - .onChange(of: rxGpio) { _ in - handleChanges() + .onChange(of: rxGpio) { newRxGpio in + if newRxGpio != node?.positionConfig?.rxGpio ?? 0 { hasChanges = true } } - .onChange(of: txGpio) { _ in - handleChanges() + .onChange(of: txGpio) { newTxGpio in + if newTxGpio != node?.positionConfig?.txGpio ?? 0 { hasChanges = true } } - .onChange(of: gpsEnGpio) { _ in - handleChanges() + .onChange(of: gpsEnGpio) { newGpsEnGpio in + if newGpsEnGpio != node?.positionConfig?.gpsEnGpio ?? 0 { hasChanges = true } } - .onChange(of: smartPositionEnabled) { _ in - handleChanges() + .onChange(of: smartPositionEnabled) { newSmartPositionEnabled in + if newSmartPositionEnabled != node?.positionConfig?.smartPositionEnabled { hasChanges = true } } - .onChange(of: positionBroadcastSeconds) { _ in - handleChanges() + .onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in + if newPositionBroadcastSeconds != node?.positionConfig?.positionBroadcastSeconds ?? 0 { hasChanges = true } } - .onChange(of: broadcastSmartMinimumIntervalSecs) { _ in - handleChanges() + .onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in + if newBroadcastSmartMinimumIntervalSecs != node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 0 { hasChanges = true } } - .onChange(of: broadcastSmartMinimumDistance) { _ in - handleChanges() + .onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in + if newBroadcastSmartMinimumDistance != node?.positionConfig?.broadcastSmartMinimumDistance ?? 0 { hasChanges = true } } - .onChange(of: gpsUpdateInterval) { _ in - handleChanges() - } - .onChange(of: positionFlags) { _ in - handleChanges() + .onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in + if newGpsUpdateInterval != node?.positionConfig?.gpsUpdateInterval ?? 0 { hasChanges = true } } } - func handleChanges() { + func handlePositionFlagtChanges() { guard let positionConfig = node?.positionConfig else { return } let pf = PositionFlags(rawValue: self.positionFlags) - hasChanges = positionConfig.deviceGpsEnabled != deviceGpsEnabled || - positionConfig.gpsMode != gpsMode || - positionConfig.rxGpio != rxGpio || - positionConfig.txGpio != txGpio || - positionConfig.gpsEnGpio != gpsEnGpio || - positionConfig.smartPositionEnabled != smartPositionEnabled || - positionConfig.positionBroadcastSeconds != positionBroadcastSeconds || - positionConfig.broadcastSmartMinimumIntervalSecs != broadcastSmartMinimumIntervalSecs || - positionConfig.broadcastSmartMinimumDistance != broadcastSmartMinimumDistance || - positionConfig.gpsUpdateInterval != gpsUpdateInterval || + hasChanges = pf.contains(.Altitude) || pf.contains(.AltitudeMsl) || pf.contains(.Satsinview) || diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 8f67d6bd..4249aab1 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -136,12 +136,12 @@ struct PowerConfig: View { } } .onChange(of: isPowerSaving) { - if let val = node?.powerConfig?.isPowerSaving { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.isPowerSaving { hasChanges = true } } - .onChange(of: shutdownOnPowerLoss) { _ in - hasChanges = true + .onChange(of: shutdownOnPowerLoss) { newShutdownOnPowerLoss in + if newShutdownOnPowerLoss { + hasChanges = true + } } .onChange(of: shutdownAfterSecs) { if let val = node?.powerConfig?.onBatteryShutdownAfterSecs { @@ -151,10 +151,8 @@ struct PowerConfig: View { .onChange(of: adcOverride) { _ in hasChanges = true } - .onChange(of: adcMultiplier) { - if let val = node?.powerConfig?.adcMultiplierOverride { - hasChanges = $0 != val - } + .onChange(of: adcMultiplier) { newAdcMultiplier in + if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? 0 { hasChanges = true } } .onChange(of: waitBluetoothSecs) { if let val = node?.powerConfig?.waitBluetoothSecs { From 61a6776b716b4b28139b14ed59d1978687894cfe Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 18:18:27 -0700 Subject: [PATCH 074/333] Fix merge conflict --- Meshtastic/Views/ContentView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 4ab6ac85..e8384c35 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -6,8 +6,11 @@ import SwiftUI @available(iOS 17.0, *) struct ContentView: View { - @ObservedObject var appState: AppState - @ObservedObject var router: Router + @ObservedObject + var appState: AppState + + @ObservedObject + var router: Router var body: some View { TabView(selection: Binding( From 3e1c5b61190f628d37ca29c768c5b79dc7f53875 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:13:45 -0700 Subject: [PATCH 075/333] Created Error handling and a new Node Position Intent --- Localizable.xcstrings | 23 +++++++- Meshtastic.xcodeproj/project.pbxproj | 8 +++ Meshtastic/AppIntents/AppIntentErrors.swift | 22 ++++++++ .../AppIntents/MessageChannelIntent.swift | 14 ++++- .../AppIntents/NodePositionIntent.swift | 56 +++++++++++++++++++ .../AppIntents/SendWaypointIntent.swift | 10 +++- 6 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 Meshtastic/AppIntents/AppIntentErrors.swift create mode 100644 Meshtastic/AppIntents/NodePositionIntent.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 495575cc..a3bd1133 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4956,6 +4956,9 @@ } } } + }, + "Could not find node" : { + }, "Counter Clockwise Rotary Event" : { @@ -6984,6 +6987,9 @@ }, "Erase all device and app data?" : { + }, + "Error: %@" : { + }, "ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message." : { @@ -7186,7 +7192,7 @@ "Factory reset your device and app? " : { }, - "Failed to encode message content." : { + "Failed to encode message content" : { }, "Failed to get a valid position to exchange" : { @@ -7200,6 +7206,9 @@ }, "Favorites" : { + }, + "Fetch the latest position of a cetain node" : { + }, "Fifteen Minutes" : { @@ -7614,6 +7623,9 @@ }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { + }, + "Get Node Position" : { + }, "Get NRF DFU from the App Store" : { @@ -15325,6 +15337,9 @@ }, "Newer firmware is available" : { + }, + "No Connected Node" : { + }, "No Device Metrics" : { @@ -15406,6 +15421,9 @@ } } } + }, + "Node does not have positions" : { + }, "Node History" : { @@ -19025,6 +19043,9 @@ }, "Send a channel message" : { + }, + "Send a message to a certain meshtastic channel" : { + }, "Send a waypoint" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0bf7d536..49a91c7d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; + BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; + BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; @@ -266,6 +268,8 @@ B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; + BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; + BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -557,6 +561,8 @@ children = ( BCB613802C67290800485544 /* SendWaypointIntent.swift */, BCB613822C672A2600485544 /* MessageChannelIntent.swift */, + BCB613842C68703800485544 /* NodePositionIntent.swift */, + BCB613862C69A0FB00485544 /* AppIntentErrors.swift */, ); path = AppIntents; sourceTree = ""; @@ -1368,6 +1374,7 @@ DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, + BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */, DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, @@ -1384,6 +1391,7 @@ DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, + BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */, 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */, diff --git a/Meshtastic/AppIntents/AppIntentErrors.swift b/Meshtastic/AppIntents/AppIntentErrors.swift new file mode 100644 index 00000000..c20ead7c --- /dev/null +++ b/Meshtastic/AppIntents/AppIntentErrors.swift @@ -0,0 +1,22 @@ +// +// AppIntentErrors.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/11/24. +// + +import Foundation + +class AppIntentErrors { + enum AppIntentError: Swift.Error, CustomLocalizedStringResourceConvertible { + case notConnected + case message(_ message: String) + + var localizedStringResource: LocalizedStringResource { + switch self { + case let .message(message): return "Error: \(message)" + case .notConnected: return "No Connected Node" + } + } + } +} diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index a952e5ff..53e202a6 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -11,6 +11,8 @@ import AppIntents struct MessageChannelIntent: AppIntent { static var title: LocalizedStringResource = "Send a channel message" + static var description: IntentDescription = "Send a message to a certain meshtastic channel" + @Parameter(title: "Message") var messageContent: String @@ -22,6 +24,10 @@ struct MessageChannelIntent: AppIntent { Summary("Send \(\.$messageContent) to \(\.$channelNumber)") } func perform() async throws -> some IntentResult { + if (!BLEManager.shared.isConnected){ + throw AppIntentErrors.AppIntentError.notConnected + } + // Check if channel number is between 1 and 7 guard (0...7).contains(channelNumber) else { throw $channelNumber.needsValueError("Channel number must be between 0 and 7.") @@ -29,15 +35,17 @@ struct MessageChannelIntent: AppIntent { // Convert messageContent to data and check its length guard let messageData = messageContent.data(using: .utf8) else { - throw $messageContent.needsValueError("Failed to encode message content.") + throw AppIntentErrors.AppIntentError.message("Failed to encode message content") } if messageData.count > 228 { throw $messageContent.needsValueError("Message content exceeds 228 bytes.") } - if (BLEManager.shared.isConnected){ - BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) + + if(!BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0)){ + throw AppIntentErrors.AppIntentError.message("Failed to send message") } + return .result() } } diff --git a/Meshtastic/AppIntents/NodePositionIntent.swift b/Meshtastic/AppIntents/NodePositionIntent.swift new file mode 100644 index 00000000..496ca3e3 --- /dev/null +++ b/Meshtastic/AppIntents/NodePositionIntent.swift @@ -0,0 +1,56 @@ +// +// NodePositionIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/10/24. +// + +import Foundation +import AppIntents +import CoreLocation +import CoreData + +struct NodePositionIntent: AppIntent { + + @Parameter(title: "Node Number") + var nodeNum: Int + + static var title: LocalizedStringResource = "Get Node Position" + static var description: IntentDescription = "Fetch the latest position of a cetain node" + + + func perform() async throws -> some IntentResult & ReturnsValue { + if (!BLEManager.shared.isConnected) { + throw AppIntentErrors.AppIntentError.notConnected + } + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + do { + guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else { + throw $nodeNum.needsValueError("Could not find node") + } + + let nodeInfo = fetchedNode[0] + if let latitude = nodeInfo.latestPosition?.coordinate.latitude, + let longitude = nodeInfo.latestPosition?.coordinate.longitude { + let nodeLocation = CLLocation(latitude: latitude, longitude: longitude) + + // Reverse geocode the CLLocation to get a CLPlacemark + let geocoder = CLGeocoder() + let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation) + + if let placemark = placemarks.first { + return .result(value: placemark) + } else { + throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location") + } + } else { + throw AppIntentErrors.AppIntentError.message("Node does not have positions") + } + } catch { + throw AppIntentErrors.AppIntentError.message("Fetch Failure") + } + } + +} + diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 43cc93c8..a94e8b7d 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -27,7 +27,9 @@ struct SendWaypointIntent: AppIntent { var locationParameter: CLPlacemark func perform() async throws -> some IntentResult { - + if (!BLEManager.shared.isConnected){ + throw AppIntentErrors.AppIntentError.notConnected + } // Provide default values if parameters are nil let name = nameParameter ?? "Dropped Pin" let description = descriptionParameter ?? "" @@ -67,10 +69,12 @@ struct SendWaypointIntent: AppIntent { newWaypoint.icon = unicode newWaypoint.name = name newWaypoint.description_p = description - if (BLEManager.shared.isConnected){ - BLEManager.shared.sendWaypoint(waypoint: newWaypoint) + if(!BLEManager.shared.sendWaypoint(waypoint: newWaypoint)){ + throw AppIntentErrors.AppIntentError.message("Failed to Send Waypoint") } + + return .result() } From c0fd6cbf942ce17e9a8dbc375a0bfb9a284b121c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 20:50:48 -0700 Subject: [PATCH 076/333] Security config details, scroll dismisses keyboard,font size updates for keys --- .../Settings/Config/SecurityConfig.swift | 41 +++++++++++-------- protobufs | 2 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index a3aa1f45..f3f79734 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -13,6 +13,7 @@ import OSLog struct SecurityConfig: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var goBack @@ -38,18 +39,16 @@ struct SecurityConfig: View { VStack(alignment: .leading) { Label("Public Key", systemImage: "key") - Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") - .foregroundStyle(.secondary) - .font(.caption) - TextField( "Public Key", text: $publicKey, axis: .vertical ) - .padding(5) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() .keyboardType(.alphabet) - .foregroundColor(.secondary) + .foregroundStyle(.tertiary) .disableAutocorrection(true) .textSelection(.enabled) .background( @@ -57,50 +56,55 @@ struct SecurityConfig: View { .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) ) + Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) } VStack(alignment: .leading) { Label("Private Key", systemImage: "key.fill") - Text("Used to create a shared key with a remote device.") - .foregroundStyle(.secondary) - .font(.caption) - TextField( "Private Key", text: $privateKey, axis: .vertical ) - .padding(5) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() .disableAutocorrection(true) .keyboardType(.alphabet) - .foregroundColor(.secondary) + .foregroundStyle(.tertiary) .textSelection(.enabled) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) ) + Text("Used to create a shared key with a remote device.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) } VStack(alignment: .leading) { Label("Admin Key", systemImage: "key.viewfinder") - Text("The public key authorized to send admin messages to this node.") - .foregroundStyle(.secondary) - .font(.caption) - TextField( "Admin Key", text: $adminKey, axis: .vertical ) - .padding(5) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() .disableAutocorrection(true) .keyboardType(.alphabet) - .foregroundColor(.secondary) + .foregroundStyle(.tertiary) .textSelection(.enabled) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) ) + Text("The public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) } } Section(header: Text("Logs")) { @@ -141,6 +145,7 @@ struct SecurityConfig: View { } } } + .scrollDismissesKeyboard(.immediately) .navigationTitle("Security Config") .navigationBarItems(trailing: ZStack { ConnectedDevice( diff --git a/protobufs b/protobufs index 66eb3184..0c052b5d 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 66eb3184d5651705ae62f80bf4f65b94a62e7f06 +Subproject commit 0c052b5d25fe8ed74c675178702f20a3fbc29afa From 6c6b44fdaa43cb8345e2bee1775bc8fd530b49ba Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Aug 2024 21:20:10 -0700 Subject: [PATCH 077/333] Simplify change events to not force unwrap values --- .../Settings/Config/BluetoothConfig.swift | 4 +-- .../Views/Settings/Config/DeviceConfig.swift | 20 +++--------- .../Views/Settings/Config/DisplayConfig.swift | 24 ++++---------- .../Views/Settings/Config/LoRaConfig.swift | 32 +++++-------------- .../Views/Settings/Config/NetworkConfig.swift | 9 ++---- .../Views/Settings/Config/PowerConfig.swift | 18 +++-------- .../Settings/Config/SecurityConfig.swift | 20 +++--------- 7 files changed, 33 insertions(+), 94 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 2e12dbdb..75132df1 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -123,9 +123,7 @@ struct BluetoothConfig: View { if $0 != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true } } .onChange(of: fixedPin) { newFixedPin in - if node != nil && node?.bluetoothConfig != nil { - if newFixedPin != String(node!.bluetoothConfig!.fixedPin) { hasChanges = true } - } + if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true } } .onChange(of: deviceLoggingEnabled) { if $0 != node?.bluetoothConfig?.deviceLoggingEnabled { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index bde4d2ff..ff23c6e5 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -262,24 +262,16 @@ struct DeviceConfig: View { if $0 != node?.deviceConfig?.debugLogEnabled { hasChanges = true } } .onChange(of: buttonGPIO) { newButtonGPIO in - if node != nil && node?.deviceConfig != nil { - if newButtonGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } - } + if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } .onChange(of: buzzerGPIO) { newBuzzerGPIO in - if node != nil && node?.deviceConfig != nil { - if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } - } + if newBuzzerGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } .onChange(of: rebroadcastMode) { newRebroadcastMode in - if node != nil && node?.deviceConfig != nil { - if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true } - } + if newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true } } .onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in - if node != nil && node?.deviceConfig != nil { - if newNodeInfoBroadcastSecs != node!.deviceConfig!.nodeInfoBroadcastSecs { hasChanges = true } - } + if newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true } } .onChange(of: doubleTapAsButtonPress) { if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } @@ -288,9 +280,7 @@ struct DeviceConfig: View { if $0 != node?.deviceConfig?.isManaged { hasChanges = true } } .onChange(of: tzdef) { newTzdef in - if node != nil && node?.deviceConfig != nil { - if newTzdef != node!.deviceConfig!.tzdef { hasChanges = true } - } + if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } } } func setDeviceValues() { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index c26d741b..1f3f6de4 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -174,14 +174,10 @@ struct DisplayConfig: View { } } .onChange(of: screenOnSeconds) { newScreenSecs in - if node != nil && node!.displayConfig != nil { - if newScreenSecs != node!.displayConfig!.screenOnSeconds { hasChanges = true } - } + if newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true } } .onChange(of: screenCarouselInterval) { newCarouselSecs in - if node != nil && node!.displayConfig != nil { - if newCarouselSecs != node!.displayConfig!.screenCarouselInterval { hasChanges = true } - } + if newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true } } .onChange(of: compassNorthTop) { if $0 != node?.displayConfig?.compassNorthTop { hasChanges = true } @@ -190,27 +186,19 @@ struct DisplayConfig: View { if $0 != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true } } .onChange(of: gpsFormat) { newGpsFormat in - if node != nil && node!.displayConfig != nil { - if newGpsFormat != node!.displayConfig!.gpsFormat { hasChanges = true } - } + if newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true } } .onChange(of: flipScreen) { if $0 != node?.displayConfig?.flipScreen { hasChanges = true } } .onChange(of: oledType) { newOledType in - if node != nil && node!.displayConfig != nil { - if newOledType != node!.displayConfig!.oledType { hasChanges = true } - } + if newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true } } .onChange(of: displayMode) { newDisplayMode in - if node != nil && node!.displayConfig != nil { - if newDisplayMode != node!.displayConfig!.displayMode { hasChanges = true } - } + if newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true } } .onChange(of: units) { newUnits in - if node != nil && node!.displayConfig != nil { - if newUnits != node!.displayConfig!.units { hasChanges = true } - } + if newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true } } } func setDisplayValues() { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 24a79c31..0a309815 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -241,42 +241,28 @@ struct LoRaConfig: View { } } .onChange(of: region) { newRegion in - if node != nil && node!.loRaConfig != nil { - if newRegion != node!.loRaConfig!.regionCode { hasChanges = true } - } + if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true } } .onChange(of: usePreset) { if $0 != node?.loRaConfig?.usePreset { hasChanges = true } } .onChange(of: modemPreset) { newModemPreset in - if node != nil && node!.loRaConfig != nil { - if newModemPreset != node!.loRaConfig!.modemPreset { hasChanges = true } - } + if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true } } .onChange(of: hopLimit) { newHopLimit in - if node != nil && node!.loRaConfig != nil { - if newHopLimit != node!.loRaConfig!.hopLimit { hasChanges = true } - } + if newHopLimit != node?.loRaConfig?.hopLimit ?? -1 { hasChanges = true } } .onChange(of: channelNum) { newChannelNum in - if node != nil && node!.loRaConfig != nil { - if newChannelNum != node!.loRaConfig!.channelNum { hasChanges = true } - } + if newChannelNum != node?.loRaConfig?.channelNum ?? -1 { hasChanges = true } } .onChange(of: bandwidth) { newBandwidth in - if node != nil && node!.loRaConfig != nil { - if newBandwidth != node!.loRaConfig!.bandwidth { hasChanges = true } - } + if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true } } .onChange(of: codingRate) { newCodingRate in - if node != nil && node!.loRaConfig != nil { - if newCodingRate != node!.loRaConfig!.codingRate { hasChanges = true } - } + if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true } } .onChange(of: spreadFactor) { newSpreadFactor in - if node != nil && node!.loRaConfig != nil { - if newSpreadFactor != node!.loRaConfig!.spreadFactor { hasChanges = true } - } + if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true } } .onChange(of: rxBoostedGain) { if $0 != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true } @@ -285,9 +271,7 @@ struct LoRaConfig: View { if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true } } .onChange(of: txPower) { newTxPower in - if node != nil && node!.loRaConfig != nil { - if newTxPower != node!.loRaConfig!.txPower { hasChanges = true } - } + if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true } } .onChange(of: txEnabled) { if $0 != node?.loRaConfig?.txEnabled { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index fb260a69..51981606 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -132,14 +132,11 @@ struct NetworkConfig: View { if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true } } .onChange(of: wifiSsid) { newSSID in - if node != nil && node!.networkConfig != nil { - if newSSID != node!.networkConfig!.wifiSsid { hasChanges = true } - } + if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true } + } .onChange(of: wifiPsk) { newPsk in - if node != nil && node!.networkConfig != nil { - if newPsk != node!.networkConfig!.wifiPsk { hasChanges = true } - } + if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true } } .onChange(of: wifiMode) { if $0 != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 4249aab1..42b6a534 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -144,30 +144,22 @@ struct PowerConfig: View { } } .onChange(of: shutdownAfterSecs) { - if let val = node?.powerConfig?.onBatteryShutdownAfterSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } .onChange(of: adcOverride) { _ in hasChanges = true } .onChange(of: adcMultiplier) { newAdcMultiplier in - if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? 0 { hasChanges = true } + if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true } } .onChange(of: waitBluetoothSecs) { - if let val = node?.powerConfig?.waitBluetoothSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true } } .onChange(of: lsSecs) { - if let val = node?.powerConfig?.lsSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true } } .onChange(of: minWakeSecs) { - if let val = node?.powerConfig?.minWakeSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index f3f79734..9877bb3a 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -155,29 +155,19 @@ struct SecurityConfig: View { ) }) .onChange(of: isManaged) { - if let val = node?.securityConfig?.isManaged { - hasChanges = $0 != val - } + if $0 != node?.securityConfig?.isManaged { hasChanges = true } } .onChange(of: serialEnabled) { - if let val = node?.securityConfig?.serialEnabled { - hasChanges = $0 != val - } + if $0 != node?.securityConfig?.serialEnabled { hasChanges = true } } .onChange(of: debugLogApiEnabled) { - if let val = node?.securityConfig?.debugLogApiEnabled { - hasChanges = $0 != val - } + if $0 != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } } .onChange(of: bluetoothLoggingEnabled) { - if let val = node?.securityConfig?.bluetoothLoggingEnabled { - hasChanges = $0 != val - } + if $0 != node?.securityConfig?.bluetoothLoggingEnabled { hasChanges = true } } .onChange(of: adminChannelEnabled) { - if let val = node?.securityConfig?.adminChannelEnabled { - hasChanges = $0 != val - } + if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { From d175f960cd2ed30f3a2a018ed89e1258783bdacb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 05:20:19 -0700 Subject: [PATCH 078/333] Hook up new key values to messages --- Meshtastic/Helpers/MeshPackets.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index c210082f..126b0401 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -820,6 +820,8 @@ func textMessageAppPacket( newMessage.isEmoji = packet.decoded.emoji == 1 newMessage.channel = Int32(packet.channel) newMessage.portNum = Int32(packet.decoded.portnum.rawValue) + newMessage.publicKey = packet.publicKey + newMessage.pkiEncrypted = packet.pkiEncrypted if packet.decoded.portnum == PortNum.detectionSensorApp { if !UserDefaults.enableDetectionNotifications { newMessage.read = true From 1942f0e2dd6ceeb6cfa998bd9e79f0f3214059d7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 05:55:48 -0700 Subject: [PATCH 079/333] Add pki info to message context menu --- Meshtastic/Views/Messages/MessageContextMenuItems.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index c9c37cdc..07792a9a 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -15,6 +15,10 @@ struct MessageContextMenuItems: View { VStack { if message.pkiEncrypted { Label("Encrypted", systemImage: "lock") + Text("Public Key") + Text(message.publicKey?.base64EncodedString() ?? "No Key") + .allowsTightening(true) + .monospaced() } Text("channel") + Text(": \(message.channel)") } From b1f1a996ca5f7de4768fc92dc6e2c89b4f283682 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 10:29:15 -0700 Subject: [PATCH 080/333] Set key values on use when a nodeinfo packet comes in over the mesh --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index e8563315..d52131b4 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -178,6 +178,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + newUser.pkiEncrypted = packet.pkiEncrypted + newUser.publicKey = packet.publicKey Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) From 9eb64bb3ed413eeddaac58b321fb4d76a34291e1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 10:32:47 -0700 Subject: [PATCH 081/333] Set public key for nodedb packets --- Meshtastic/Helpers/MeshPackets.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 126b0401..963465b9 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -299,6 +299,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) + if !nodeInfo.user.publicKey.isEmpty { + newUser.pkiEncrypted = true + newUser.publicKey = nodeInfo.user.publicKey + } newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { let newUser = createUser(num: Int64(nodeInfo.num), context: context) From 6926d857ff2ed23d22a9892f6e4b9957f05e8798 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 14:42:47 -0700 Subject: [PATCH 082/333] Add secure input view to handle key inputs --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Helpers/MeshPackets.swift | 2 + Meshtastic/Views/Helpers/SecureInput.swift | 57 +++++++++++++++++++ .../Settings/Config/SecurityConfig.swift | 55 +----------------- 4 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 Meshtastic/Views/Helpers/SecureInput.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index f3c62dc1..f5108298 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; + DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; @@ -345,6 +346,7 @@ DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; + DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; @@ -912,6 +914,7 @@ DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */, DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */, DD5E523D298F5A7D00D21B61 /* Weather */, + DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, ); path = Helpers; sourceTree = ""; @@ -1281,6 +1284,7 @@ DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, + DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 963465b9..ceefb253 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -842,6 +842,8 @@ func textMessageAppPacket( } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) + newMessage.fromUser?.publicKey = packet.publicKey + newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift new file mode 100644 index 00000000..b4fa54eb --- /dev/null +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -0,0 +1,57 @@ +// +// SecureInput.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/12/24. +// + +import SwiftUI + +struct SecureInput: View { + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Binding private var text: String + @State private var isSecure: Bool = true + private var title: String + + init(_ title: String, text: Binding) { + self.title = title + self._text = text + } + + var body: some View { + ZStack(alignment: .trailing) { + Group { + if isSecure { + SecureField(title, text: $text) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() + .keyboardType(.alphabet) + .foregroundStyle(.tertiary) + .disableAutocorrection(true) + .textSelection(.enabled) + } else { + TextField(title, text: $text, axis: .vertical) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() + .keyboardType(.alphabet) + .foregroundStyle(.tertiary) + .disableAutocorrection(true) + .textSelection(.enabled) + .lineLimit(...3) + } + }.padding(.trailing, 36) + + if !text.isEmpty { + Button(action: { + isSecure.toggle() + }) { + Image(systemName: self.isSecure ? "eye.slash" : "eye") + .accentColor(.secondary) + } + } + } + } +} diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 9877bb3a..d031306b 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -38,70 +38,21 @@ struct SecurityConfig: View { Section(header: Text("Admin & Direct Message Keys")) { VStack(alignment: .leading) { Label("Public Key", systemImage: "key") - - TextField( - "Public Key", - text: $publicKey, - axis: .vertical - ) - .font(idiom == .phone ? .caption : .callout) - .allowsTightening(true) - .monospaced() - .keyboardType(.alphabet) - .foregroundStyle(.tertiary) - .disableAutocorrection(true) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) - ) - + SecureInput("Public Key", text: $publicKey) Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } VStack(alignment: .leading) { Label("Private Key", systemImage: "key.fill") - - TextField( - "Private Key", - text: $privateKey, - axis: .vertical - ) - .font(idiom == .phone ? .caption : .callout) - .allowsTightening(true) - .monospaced() - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundStyle(.tertiary) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) - ) + SecureInput("Private Key", text: $privateKey) Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } VStack(alignment: .leading) { Label("Admin Key", systemImage: "key.viewfinder") - - TextField( - "Admin Key", - text: $adminKey, - axis: .vertical - ) - .font(idiom == .phone ? .caption : .callout) - .allowsTightening(true) - .monospaced() - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundStyle(.tertiary) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(true ? Color.clear : Color.red, lineWidth: 2.0) - ) + SecureInput("Private Key", text: $adminKey) Text("The public key authorized to send admin messages to this node.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) From 8dc4f3076dec3741b583c1c63fc364e47987a942 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 15:27:10 -0700 Subject: [PATCH 083/333] Set a key for an existing user if the key is empty --- Meshtastic/Helpers/MeshPackets.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index ceefb253..809bdbcc 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -360,6 +360,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if fetchedNode[0].user == nil { fetchedNode[0].user = UserEntity(context: context) } + // Set the public key for a user if it is empty, don't update + if fetchedNode[0].user?.publicKey?.isEmpty == nil && !nodeInfo.user.publicKey.isEmpty { + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey + } fetchedNode[0].user!.userId = nodeInfo.user.id fetchedNode[0].user!.num = Int64(nodeInfo.num) fetchedNode[0].user!.numString = String(nodeInfo.num) From e633009e9dd6f2e033c82f2e50f852fd2e1ff93c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 16:46:25 -0700 Subject: [PATCH 084/333] Icons for each messge type --- Meshtastic/Views/Messages/UserList.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 392ce4d0..361407de 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -28,6 +28,7 @@ struct UserList: View { @State private var roleFilter = false @State private var deviceRoles: Set = [] @State var isEditingFilters = false + @State private var showingTrustConfirm: Bool = false @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), @@ -71,8 +72,17 @@ struct UserList: View { VStack(alignment: .leading) { HStack { if user.pkiEncrypted { - Image(systemName: "lock.fill") - .foregroundColor(.green) + if mostRecent == nil || user.publicKey == mostRecent?.publicKey { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } else { + /// Public Key on the User and the Public Key on the Last Message don't match + Image(systemName: "lock.slash.fill") + .foregroundColor(.red) + } + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) } Text(user.longName ?? "unknown".localized) .font(.headline) @@ -245,7 +255,6 @@ struct UserList: View { 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 From b3f32238c894a4bbae3f588779417ff2f3283f4d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 16:56:33 -0700 Subject: [PATCH 085/333] add keyMatch value to user --- Meshtastic/Helpers/MeshPackets.swift | 12 ++++++++++-- .../MeshtasticDataModelV 42.xcdatamodel/contents | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 809bdbcc..96db5fd2 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -847,8 +847,16 @@ func textMessageAppPacket( } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - newMessage.fromUser?.publicKey = packet.publicKey - newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted + if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) { + /// We have a key, check if it matches + if newMessage.fromUser?.publicKey != newMessage.publicKey { + newMessage.fromUser?.keyMatch = false + } + } else { + /// We have no key, set it + newMessage.fromUser?.publicKey = packet.publicKey + newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted + } if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index 432be802..b92f33f6 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -432,6 +432,7 @@ + From edde940a792614409811a5fb9ed98d936c90e06a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 17:27:43 -0700 Subject: [PATCH 086/333] Use key match --- Meshtastic/Views/Messages/UserList.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 361407de..0bc00124 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -72,13 +72,13 @@ struct UserList: View { VStack(alignment: .leading) { HStack { if user.pkiEncrypted { - if mostRecent == nil || user.publicKey == mostRecent?.publicKey { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } else { + if !user.keyMatch { /// Public Key on the User and the Public Key on the Last Message don't match Image(systemName: "lock.slash.fill") .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) } } else { Image(systemName: "lock.open.fill") From 772a14d664325bd59f590a7517f4248e676a567e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 19:51:34 -0700 Subject: [PATCH 087/333] Add pki encryption toggle to user and node list filters --- Meshtastic/Views/Messages/UserList.swift | 11 ++++++++++- .../Views/Nodes/Helpers/NodeListFilter.swift | 16 +++++++++++++++- Meshtastic/Views/Nodes/NodeList.swift | 10 +++++++++- protobufs | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 0bc00124..c2ede597 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -20,6 +20,7 @@ struct UserList: View { @State private var viaLora = true @State private var viaMqtt = true @State private var isOnline = false + @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isEnvironment = false @State private var distanceFilter = false @@ -186,7 +187,7 @@ struct UserList: View { .listStyle(.plain) .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1))) .sheet(isPresented: $isEditingFilters) { - NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) + NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } .onChange(of: searchText) { _ in searchUserList() @@ -212,6 +213,9 @@ struct UserList: View { .onChange(of: isOnline) { _ in searchUserList() } + .onChange(of: isPkiEncrypted) { _ in + searchUserList() + } .onChange(of: isFavorite) { _ in searchUserList() } @@ -292,6 +296,11 @@ struct UserList: View { let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } + /// Encrypted + if isPkiEncrypted { + let isPkiEncryptedPredicate = NSPredicate(format: "pkiEncrypted == YES") + predicates.append(isPkiEncryptedPredicate) + } /// Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 6236f7b7..20435dd0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -15,6 +15,7 @@ struct NodeListFilter: View { @Binding var viaLora: Bool @Binding var viaMqtt: Bool @Binding var isOnline: Bool + @Binding var isPkiEncrypted: Bool @Binding var isFavorite: Bool @Binding var isEnvironment: Bool @Binding var distanceFilter: Bool @@ -64,6 +65,19 @@ struct NodeListFilter: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) + Toggle(isOn: $isPkiEncrypted) { + + Label { + Text("Encrypted") + } icon: { + Image(systemName: "lock.fill") + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + Toggle(isOn: $isFavorite) { Label { @@ -173,7 +187,7 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.medium, .large]) + .presentationDetents([.fraction(0.7), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) .presentationBackgroundInteraction(.enabled(upThrough: .medium)) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index e05cb34b..f85cad48 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -24,6 +24,7 @@ struct NodeList: View { @State private var viaLora = true @State private var viaMqtt = true @State private var isOnline = false + @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isEnvironment = false @State private var distanceFilter = false @@ -38,8 +39,9 @@ struct NodeList: View { @State private var deleteNodeId: Int64 = 0 var boolFilters: [Bool] {[ - isOnline, isFavorite, + isOnline, + isPkiEncrypted, isEnvironment, distanceFilter, roleFilter @@ -153,6 +155,7 @@ struct NodeList: View { viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, + isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, @@ -383,6 +386,11 @@ struct NodeList: View { let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } + /// Encrypted + if isPkiEncrypted { + let isPkiEncryptedPredicate = NSPredicate(format: "user.pkiEncrypted == YES") + predicates.append(isPkiEncryptedPredicate) + } /// Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "favorite == YES") diff --git a/protobufs b/protobufs index 0c052b5d..3e753697 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 0c052b5d25fe8ed74c675178702f20a3fbc29afa +Subproject commit 3e753697aa1140d2c998cb63739729e733002874 From c1e625f7f9bed34585d1a4a023b28ec73efdceab Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Aug 2024 20:02:11 -0700 Subject: [PATCH 088/333] filter touch up --- Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 20435dd0..eaaf7016 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -187,9 +187,9 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.fraction(0.7), .large]) + .presentationDetents([.fraction(0.65), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) - .presentationBackgroundInteraction(.enabled(upThrough: .medium)) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } From 9368156eff825d7a203f5f31afea66ee6c6f4c30 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Aug 2024 08:24:34 -0700 Subject: [PATCH 089/333] Use public key from user on outgoiong messages --- Meshtastic/Helpers/BLEManager.swift | 4 ++++ Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index fdbd2d52..92e1f94d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1000,6 +1000,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if toUserNum > 0 { newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) newMessage.toUser?.lastMessage = Date() + if newMessage.toUser?.pkiEncrypted ?? false { + newMessage.publicKey = newMessage.toUser?.publicKey + newMessage.pkiEncrypted = true + } } newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.isEmoji = isEmoji diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index eaaf7016..66d88b28 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -187,7 +187,7 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.fraction(0.65), .large]) + .presentationDetents([.fraction(0.75), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) .presentationBackgroundInteraction(.enabled(upThrough: .large)) From 63b845fd62d5211fa37824c4692087de4dbd0efa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Aug 2024 14:02:57 -0700 Subject: [PATCH 090/333] send public key with new messages --- Meshtastic/Helpers/BLEManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 92e1f94d..4e7489dc 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1026,6 +1026,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate dataMessage.portnum = dataType var meshPacket = MeshPacket() + if newMessage.toUser?.pkiEncrypted ?? false { + meshPacket.pkiEncrypted = true + meshPacket.publicKey = newMessage.toUser?.publicKey ?? Data() + } meshPacket.id = UInt32(newMessage.messageId) if toUserNum > 0 { meshPacket.to = UInt32(toUserNum) From 5087430f02cf9d92e0649390e11e01337b399ef6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Aug 2024 16:44:29 -0700 Subject: [PATCH 091/333] Bump firmware version, remove outdated ble pairing information for factory reset --- Localizable.xcstrings | 2 +- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 2 +- Meshtastic/Views/Settings/Firmware.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b704efe3..6160d6ae 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -709,7 +709,7 @@ "All" : { }, - "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth." : { + "All device and app data will be deleted." : { }, "Allow incoming device control over the insecure legacy admin channel." : { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index ff23c6e5..106614d2 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -183,7 +183,7 @@ struct DeviceConfig: View { .controlSize(.regular) .padding(.trailing) .confirmationDialog( - "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth.", + "All device and app data will be deleted.", isPresented: $isPresentingFactoryResetConfirm, titleVisibility: .visible ) { diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index e591761f..d76b1503 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -13,7 +13,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.4.0" + @State var minimumVersion = "2.4.3" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? From affdd3954229156a64fe2290b22d830ae20e2ecc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 03:10:12 -0700 Subject: [PATCH 092/333] Add onFirstAppear extension --- Meshtastic.xcodeproj/project.pbxproj | 4 +++ Meshtastic/Extensions/View.swift | 30 +++++++++++++++++++ .../Views/Settings/Config/ConfigHeader.swift | 4 +-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Extensions/View.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index f5108298..c891cca9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; + DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; @@ -347,6 +348,7 @@ DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; + DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; @@ -1001,6 +1003,7 @@ DDD5BB172C2F9C36007E03CA /* OSLogEntryLog.swift */, DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, DDD5BB0C2C285F00007E03CA /* Logger.swift */, + DD6F65732C6CB80A0053C113 /* View.swift */, ); path = Extensions; sourceTree = ""; @@ -1374,6 +1377,7 @@ DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */, + DD6F65742C6CB80A0053C113 /* View.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, diff --git a/Meshtastic/Extensions/View.swift b/Meshtastic/Extensions/View.swift new file mode 100644 index 00000000..7349f36d --- /dev/null +++ b/Meshtastic/Extensions/View.swift @@ -0,0 +1,30 @@ +// +// View.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/14/24. +// + +import SwiftUI + +public extension View { + func onFirstAppear(_ action: @escaping () -> ()) -> some View { + modifier(FirstAppear(action: action)) + } +} + +private struct FirstAppear: ViewModifier { + let action: () -> () + + // Use this to only fire your block one time + @State private var hasAppeared = false + + func body(content: Content) -> some View { + // And then, track it here + content.onAppear { + guard !hasAppeared else { return } + hasAppeared = true + action() + } + } +} diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index cef7f98c..3f59a01a 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -23,12 +23,12 @@ struct ConfigHeader: View { .foregroundColor(.orange) } else { Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") - .onAppear(perform: onAppear) + .onFirstAppear(onAppear) .font(.title3) } } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 { Text("Configuration for: \(node?.user?.longName ?? "Unknown")") - .onAppear(perform: onAppear) + .onFirstAppear(onAppear) } else { Text("Please connect to a radio to configure settings.") .font(.callout) From 1fdbd5cc3a23e7f7dd6723f32770696a0e5fb067 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 03:52:20 -0700 Subject: [PATCH 093/333] Switch to broken key icon for key match --- Meshtastic/Views/Messages/UserList.swift | 2 +- Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index c2ede597..4d7b2fd9 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -75,7 +75,7 @@ struct UserList: View { if user.pkiEncrypted { if !user.keyMatch { /// Public Key on the User and the Public Key on the Last Message don't match - Image(systemName: "lock.slash.fill") + Image(systemName: "key.slash") .foregroundColor(.red) } else { Image(systemName: "lock.fill") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index ba7dee6e..10829f31 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -145,7 +145,6 @@ struct NodeListItem: View { HStack { Image(systemName: "\(node.channel).circle.fill") .font(.title2) - .symbolRenderingMode(.multicolor) .frame(width: 30) Text("Channel") .foregroundColor(.secondary) @@ -216,7 +215,6 @@ struct NodeListItem: View { .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) Image(systemName: "\(node.hopsAway).square") .font(.title2) - .symbolRenderingMode(.multicolor) } } else { if node.snr != 0 && !node.viaMqtt { From 1c58539206e5f76bdd2ce4041de5c22d4880e7db Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Aug 2024 12:43:26 -0500 Subject: [PATCH 094/333] Remove git shenanigans from gen_protos script --- scripts/gen_protos.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scripts/gen_protos.sh b/scripts/gen_protos.sh index a587a8e3..d07bc798 100755 --- a/scripts/gen_protos.sh +++ b/scripts/gen_protos.sh @@ -1,12 +1,5 @@ #!/bin/bash -# simple sanity checking for repo -if [ ! -d "./protobufs" ]; then - git submodule update --init -else - git submodule update --remote --merge -fi - # simple sanity checking for executable if [ ! -x "$(which protoc)" ]; then brew install swift-protobuf From d987c09409bdf65602b4baca431aa0b9e001c006 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Aug 2024 12:45:03 -0500 Subject: [PATCH 095/333] Update protos on 2.5 --- .../Sources/meshtastic/admin.pb.swift | 267 ++------- .../Sources/meshtastic/apponly.pb.swift | 6 +- .../Sources/meshtastic/atak.pb.swift | 64 +-- .../meshtastic/cannedmessages.pb.swift | 6 +- .../Sources/meshtastic/channel.pb.swift | 37 +- .../Sources/meshtastic/clientonly.pb.swift | 6 +- .../Sources/meshtastic/config.pb.swift | 445 ++++++--------- .../meshtastic/connection_status.pb.swift | 21 +- .../Sources/meshtastic/deviceonly.pb.swift | 154 ++++- .../Sources/meshtastic/localonly.pb.swift | 9 +- .../Sources/meshtastic/mesh.pb.swift | 530 +++++------------- .../Sources/meshtastic/module_config.pb.swift | 263 +++------ .../Sources/meshtastic/mqtt.pb.swift | 9 +- .../Sources/meshtastic/paxcount.pb.swift | 6 +- .../Sources/meshtastic/portnums.pb.swift | 14 +- .../Sources/meshtastic/powermon.pb.swift | 115 ++-- .../meshtastic/remote_hardware.pb.swift | 35 +- .../Sources/meshtastic/rtttl.pb.swift | 6 +- .../Sources/meshtastic/storeforward.pb.swift | 93 +-- .../Sources/meshtastic/telemetry.pb.swift | 87 ++- .../Sources/meshtastic/xmodem.pb.swift | 39 +- protobufs | 2 +- 22 files changed, 747 insertions(+), 1467 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 3ea4ac4c..4028fdcd 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -24,7 +24,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage { +public struct AdminMessage: 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. @@ -466,7 +466,7 @@ public struct AdminMessage { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -603,189 +603,11 @@ public struct AdminMessage { /// Tell the node to reset the nodedb. case nodedbReset(Int32) - #if !swift(>=4.1) - public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.getChannelRequest, .getChannelRequest): return { - guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getChannelResponse, .getChannelResponse): return { - guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerRequest, .getOwnerRequest): return { - guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerResponse, .getOwnerResponse): return { - guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigRequest, .getConfigRequest): return { - guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigResponse, .getConfigResponse): return { - guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigRequest, .getModuleConfigRequest): return { - guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigResponse, .getModuleConfigResponse): return { - guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { - guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { - guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { - guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { - guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneRequest, .getRingtoneRequest): return { - guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneResponse, .getRingtoneResponse): return { - guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { - guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { - guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setHamMode, .setHamMode): return { - guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { - guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { - guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.enterDfuModeRequest, .enterDfuModeRequest): return { - guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deleteFileRequest, .deleteFileRequest): return { - guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setScale, .setScale): return { - guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setOwner, .setOwner): return { - guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setChannel, .setChannel): return { - guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setConfig, .setConfig): return { - guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setModuleConfig, .setModuleConfig): return { - guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { - guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setRingtoneMessage, .setRingtoneMessage): return { - guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeByNodenum, .removeByNodenum): return { - guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFavoriteNode, .setFavoriteNode): return { - guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFavoriteNode, .removeFavoriteNode): return { - guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFixedPosition, .setFixedPosition): return { - guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFixedPosition, .removeFixedPosition): return { - guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.beginEditSettings, .beginEditSettings): return { - guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.commitEditSettings, .commitEditSettings): return { - guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetDevice, .factoryResetDevice): return { - guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootOtaSeconds, .rebootOtaSeconds): return { - guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.exitSimulator, .exitSimulator): return { - guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootSeconds, .rebootSeconds): return { - guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.shutdownSeconds, .shutdownSeconds): return { - guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetConfig, .factoryResetConfig): return { - guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodedbReset, .nodedbReset): return { - guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum { + public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -853,11 +675,23 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + .securityConfig, + ] + } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum { + public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -955,51 +789,31 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] + } public init() {} } -#if swift(>=4.2) - -extension AdminMessage.ConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - .securityConfig, - ] -} - -extension AdminMessage.ModuleConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] -} - -#endif // swift(>=4.2) - /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters { +public struct HamParameters: 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. @@ -1029,7 +843,7 @@ public struct HamParameters { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse { +public struct NodeRemoteHardwarePinsResponse: 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. @@ -1043,15 +857,6 @@ public struct NodeRemoteHardwarePinsResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension AdminMessage: @unchecked Sendable {} -extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension AdminMessage.ConfigType: @unchecked Sendable {} -extension AdminMessage.ModuleConfigType: @unchecked Sendable {} -extension HamParameters: @unchecked Sendable {} -extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1763,7 +1568,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency != 0 { + if self.frequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 0457077c..18e66d8e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet { +public struct ChannelSet: 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. @@ -53,10 +53,6 @@ public struct ChannelSet { fileprivate var _loraConfig: Config.LoRaConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSet: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 4406deb3..1dd12469 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum { +public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -130,11 +130,6 @@ public enum Team: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -153,13 +148,12 @@ extension Team: CaseIterable { .darkGreen, .brown, ] -} -#endif // swift(>=4.2) +} /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum { +public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -233,11 +227,6 @@ public enum MemberRole: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -250,13 +239,12 @@ extension MemberRole: CaseIterable { .rto, .k9, ] -} -#endif // swift(>=4.2) +} /// /// Packets for the official ATAK Plugin -public struct TAKPacket { +public struct TAKPacket: 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. @@ -326,7 +314,7 @@ public struct TAKPacket { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TAK position report case pli(PLI) @@ -334,24 +322,6 @@ public struct TAKPacket { /// ATAK GeoChat message case chat(GeoChat) - #if !swift(>=4.1) - public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.pli, .pli): return { - guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.chat, .chat): return { - guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -363,7 +333,7 @@ public struct TAKPacket { /// /// ATAK GeoChat message -public struct GeoChat { +public struct GeoChat: 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. @@ -405,7 +375,7 @@ public struct GeoChat { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group { +public struct Group: 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. @@ -427,7 +397,7 @@ public struct Group { /// /// ATAK EUD Status /// -public struct Status { +public struct Status: 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. @@ -444,7 +414,7 @@ public struct Status { /// /// ATAK Contact /// -public struct Contact { +public struct Contact: 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. @@ -464,7 +434,7 @@ public struct Contact { /// /// Position Location Information from ATAK -public struct PLI { +public struct PLI: 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. @@ -496,18 +466,6 @@ public struct PLI { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Team: @unchecked Sendable {} -extension MemberRole: @unchecked Sendable {} -extension TAKPacket: @unchecked Sendable {} -extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension GeoChat: @unchecked Sendable {} -extension Group: @unchecked Sendable {} -extension Status: @unchecked Sendable {} -extension Contact: @unchecked Sendable {} -extension PLI: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..a43393e1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig { +public struct CannedMessageModuleConfig: 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. @@ -36,10 +36,6 @@ public struct CannedMessageModuleConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension CannedMessageModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index 5b9c7e49..a8c96595 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -36,13 +36,15 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings { +public struct ChannelSettings: @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. /// /// Deprecated in favor of LoraConfig.channel_num + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -111,7 +113,7 @@ public struct ChannelSettings { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings { +public struct ModuleSettings: 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. @@ -132,7 +134,7 @@ public struct ModuleSettings { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel { +public struct Channel: 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. @@ -170,7 +172,7 @@ public struct Channel { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -209,6 +211,13 @@ public struct Channel { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] + } public init() {} @@ -216,26 +225,6 @@ public struct Channel { fileprivate var _settings: ChannelSettings? = nil } -#if swift(>=4.2) - -extension Channel.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSettings: @unchecked Sendable {} -extension ModuleSettings: @unchecked Sendable {} -extension Channel: @unchecked Sendable {} -extension Channel.Role: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index c3d93bf7..89370cc5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile { +public struct DeviceProfile: 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. @@ -94,10 +94,6 @@ public struct DeviceProfile { fileprivate var _moduleConfig: LocalModuleConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceProfile: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 4b953470..ecfeeefa 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config { +public struct Config: 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. @@ -97,7 +97,7 @@ public struct Config { /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -107,53 +107,11 @@ public struct Config { case bluetooth(Config.BluetoothConfig) case security(Config.SecurityConfig) - #if !swift(>=4.1) - public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.device, .device): return { - guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.position, .position): return { - guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.power, .power): return { - guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.network, .network): return { - guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.display, .display): return { - guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lora, .lora): return { - guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.bluetooth, .bluetooth): return { - guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.security, .security): return { - guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// Configuration - public struct DeviceConfig { + public struct DeviceConfig: 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. @@ -165,12 +123,16 @@ public struct Config { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var serialEnabled: Bool = false /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). /// Set this to true to leave the debug log outputting even when API is active. /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var debugLogEnabled: Bool = false /// @@ -200,6 +162,8 @@ public struct Config { /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var isManaged: Bool = false /// @@ -218,7 +182,7 @@ public struct Config { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -236,6 +200,8 @@ public struct Config { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 + + /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -326,11 +292,26 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + ] + } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum { + public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -378,6 +359,14 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + ] + } public init() {} @@ -385,7 +374,7 @@ public struct Config { /// /// Position Config - public struct PositionConfig { + public struct PositionConfig: 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. @@ -407,6 +396,8 @@ public struct Config { /// /// Is GPS enabled for this node? + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -417,6 +408,8 @@ public struct Config { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -457,7 +450,7 @@ public struct Config { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum { + public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -547,9 +540,24 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] + } - public enum GpsMode: SwiftProtobuf.Enum { + public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -587,6 +595,13 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] + } public init() {} @@ -595,7 +610,7 @@ public struct Config { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig { + public struct PowerConfig: 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. @@ -655,7 +670,7 @@ public struct Config { /// /// Network Config - public struct NetworkConfig { + public struct NetworkConfig: 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. @@ -702,7 +717,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum { + public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -734,9 +749,15 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] + } - public struct IpV4Config { + public struct IpV4Config: 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. @@ -769,7 +790,7 @@ public struct Config { /// /// Display Config - public struct DisplayConfig { + public struct DisplayConfig: 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. @@ -825,7 +846,7 @@ public struct Config { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -888,11 +909,21 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] + } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum { + public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -924,11 +955,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] + } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum { + public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -972,9 +1009,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + ] + } - public enum DisplayMode: SwiftProtobuf.Enum { + public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1018,9 +1063,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] + } - public enum CompassOrientation: SwiftProtobuf.Enum { + public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1088,6 +1141,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] + } public init() {} @@ -1095,7 +1160,7 @@ public struct Config { /// /// Lora Config - public struct LoRaConfig { + public struct LoRaConfig: @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. @@ -1252,7 +1317,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum { + public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1386,12 +1451,35 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + ] + } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum { + public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1405,6 +1493,8 @@ public struct Config { /// /// Very Long Range - Slow /// Deprecated in 2.5: Works only with txco and is unusably slow + /// + /// NOTE: This enum value was marked as deprecated in the .proto file case veryLongSlow // = 2 /// @@ -1468,6 +1558,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + .shortTurbo, + ] + } public init() {} @@ -1475,7 +1578,7 @@ public struct Config { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig { + public struct BluetoothConfig: 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. @@ -1495,11 +1598,13 @@ public struct Config { /// /// Enables device (serial style logs) over Bluetooth /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var deviceLoggingEnabled: Bool = false public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum { + public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1537,12 +1642,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] + } public init() {} } - public struct SecurityConfig { + public struct SecurityConfig: @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. @@ -1591,201 +1703,6 @@ public struct Config { public init() {} } -#if swift(>=4.2) - -extension Config.DeviceConfig.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - ] -} - -extension Config.DeviceConfig.RebroadcastMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - ] -} - -extension Config.PositionConfig.PositionFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] -} - -extension Config.PositionConfig.GpsMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] -} - -extension Config.NetworkConfig.AddressMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] -} - -extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] -} - -extension Config.DisplayConfig.DisplayUnits: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] -} - -extension Config.DisplayConfig.OledType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - ] -} - -extension Config.DisplayConfig.DisplayMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] -} - -extension Config.DisplayConfig.CompassOrientation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] -} - -extension Config.LoRaConfig.RegionCode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - ] -} - -extension Config.LoRaConfig.ModemPreset: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - .shortTurbo, - ] -} - -extension Config.BluetoothConfig.PairingMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension Config: @unchecked Sendable {} -extension Config.OneOf_PayloadVariant: @unchecked Sendable {} -extension Config.DeviceConfig: @unchecked Sendable {} -extension Config.DeviceConfig.Role: @unchecked Sendable {} -extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} -extension Config.PositionConfig: @unchecked Sendable {} -extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} -extension Config.PositionConfig.GpsMode: @unchecked Sendable {} -extension Config.PowerConfig: @unchecked Sendable {} -extension Config.NetworkConfig: @unchecked Sendable {} -extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} -extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} -extension Config.DisplayConfig: @unchecked Sendable {} -extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} -extension Config.DisplayConfig.OledType: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} -extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} -extension Config.LoRaConfig: @unchecked Sendable {} -extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} -extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} -extension Config.BluetoothConfig: @unchecked Sendable {} -extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} -extension Config.SecurityConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2260,7 +2177,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride != 0 { + if self.adcMultiplierOverride.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2704,7 +2621,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset != 0 { + if _storage._frequencyOffset.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2728,7 +2645,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency != 0 { + if _storage._overrideFrequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a2ec180e..a4569714 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus { +public struct DeviceConnectionStatus: 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus { /// /// WiFi connection status -public struct WifiConnectionStatus { +public struct WifiConnectionStatus: 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus { /// /// Ethernet connection status -public struct EthernetConnectionStatus { +public struct EthernetConnectionStatus: 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus { +public struct NetworkConnectionStatus: 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus { +public struct BluetoothConnectionStatus: 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus { /// /// Serial connection status -public struct SerialConnectionStatus { +public struct SerialConnectionStatus: 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. @@ -209,15 +209,6 @@ public struct SerialConnectionStatus { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceConnectionStatus: @unchecked Sendable {} -extension WifiConnectionStatus: @unchecked Sendable {} -extension EthernetConnectionStatus: @unchecked Sendable {} -extension NetworkConnectionStatus: @unchecked Sendable {} -extension BluetoothConnectionStatus: @unchecked Sendable {} -extension SerialConnectionStatus: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 10b9af2b..076e639e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum { +public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -60,24 +60,18 @@ public enum ScreenFonts: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, ] -} -#endif // swift(>=4.2) +} /// /// Position with static location information only for NodeDBLite -public struct PositionLite { +public struct PositionLite: 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. @@ -112,7 +106,54 @@ public struct PositionLite { public init() {} } -public struct NodeInfoLite { +public struct UserLite: @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. + + /// + /// This is the addr of the radio. + /// + /// NOTE: This field was marked as deprecated in the .proto file. + public var macaddr: Data = Data() + + /// + /// A full name for this user, i.e. "Kevin Hester" + public var longName: String = String() + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + public var shortName: String = String() + + /// + /// TBEAM, HELTEC, etc... + /// Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. + /// Apps will still need the string here for older builds + /// (so OTA update can find the right image), but if the enum is available it will be used instead. + public var hwModel: HardwareModel = .unset + + /// + /// In some regions Ham radio operators have different bandwidth limitations than others. + /// If this user is a licensed operator, set this flag. + /// Also, "long_name" should be their licence number. + public var isLicensed: Bool = false + + /// + /// Indicates that the user's role in the mesh + public var role: Config.DeviceConfig.Role = .client + + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + public var publicKey: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct NodeInfoLite: @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. @@ -126,8 +167,8 @@ public struct NodeInfoLite { /// /// The user info for this node - public var user: User { - get {return _storage._user ?? User()} + public var user: UserLite { + get {return _storage._user ?? UserLite()} set {_uniqueStorage()._user = newValue} } /// Returns true if `user` has been explicitly set. @@ -215,7 +256,7 @@ public struct NodeInfoLite { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState { +public struct DeviceState: @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. @@ -275,6 +316,8 @@ public struct DeviceState { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -323,7 +366,7 @@ public struct DeviceState { /// /// The on-disk saved channels -public struct ChannelFile { +public struct ChannelFile: 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. @@ -346,7 +389,7 @@ public struct ChannelFile { /// /// This can be used for customizing the firmware distribution. If populated, /// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore { +public struct OEMStore: @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. @@ -405,15 +448,6 @@ public struct OEMStore { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ScreenFonts: @unchecked Sendable {} -extension PositionLite: @unchecked Sendable {} -extension NodeInfoLite: @unchecked Sendable {} -extension DeviceState: @unchecked Sendable {} -extension ChannelFile: @unchecked Sendable {} -extension OEMStore: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -482,6 +516,74 @@ extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } } +extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".UserLite" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "macaddr"), + 2: .standard(proto: "long_name"), + 3: .standard(proto: "short_name"), + 4: .standard(proto: "hw_model"), + 5: .standard(proto: "is_licensed"), + 6: .same(proto: "role"), + 7: .standard(proto: "public_key"), + ] + + 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.decodeSingularBytesField(value: &self.macaddr) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.longName) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.shortName) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 7: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.macaddr.isEmpty { + try visitor.visitSingularBytesField(value: self.macaddr, fieldNumber: 1) + } + if !self.longName.isEmpty { + try visitor.visitSingularStringField(value: self.longName, fieldNumber: 2) + } + if !self.shortName.isEmpty { + try visitor.visitSingularStringField(value: self.shortName, fieldNumber: 3) + } + if self.hwModel != .unset { + try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 4) + } + if self.isLicensed != false { + try visitor.visitSingularBoolField(value: self.isLicensed, fieldNumber: 5) + } + if self.role != .client { + try visitor.visitSingularEnumField(value: self.role, fieldNumber: 6) + } + if !self.publicKey.isEmpty { + try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: UserLite, rhs: UserLite) -> Bool { + if lhs.macaddr != rhs.macaddr {return false} + if lhs.longName != rhs.longName {return false} + if lhs.shortName != rhs.shortName {return false} + if lhs.hwModel != rhs.hwModel {return false} + if lhs.isLicensed != rhs.isLicensed {return false} + if lhs.role != rhs.role {return false} + if lhs.publicKey != rhs.publicKey {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeInfoLite" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -499,7 +601,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat fileprivate class _StorageClass { var _num: UInt32 = 0 - var _user: User? = nil + var _user: UserLite? = nil var _position: PositionLite? = nil var _snr: Float = 0 var _lastHeard: UInt32 = 0 @@ -581,7 +683,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 0af27466..f2ef681d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig { +public struct LocalConfig: @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. @@ -129,7 +129,7 @@ public struct LocalConfig { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig { +public struct LocalModuleConfig: @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. @@ -293,11 +293,6 @@ public struct LocalModuleConfig { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=5.5) && canImport(_Concurrency) -extension LocalConfig: @unchecked Sendable {} -extension LocalModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 489cd8e3..c9482fc1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -25,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum { +public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -329,6 +329,19 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors. case trackerT1000E // = 71 + /// + /// RAK3172 STM32WLE5 Module (https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172) + case rak3172 // = 72 + + /// + /// Seeed Studio Wio-E5 (either mini or Dev kit) using STM32WL chip. + case wioE5 // = 73 + + /// + /// RadioMaster 900 Bandit, https://www.radiomasterrc.com/products/bandit-expresslrs-rf-module + /// SSD1306 OLED and No GPS + case radiomaster900Bandit // = 74 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -413,6 +426,9 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 69: self = .heltecMeshNodeT114 case 70: self = .sensecapIndicator case 71: self = .trackerT1000E + case 72: self = .rak3172 + case 73: self = .wioE5 + case 74: self = .radiomaster900Bandit case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -491,16 +507,14 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .heltecMeshNodeT114: return 69 case .sensecapIndicator: return 70 case .trackerT1000E: return 71 + case .rak3172: return 72 + case .wioE5: return 73 + case .radiomaster900Bandit: return 74 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -574,15 +588,17 @@ extension HardwareModel: CaseIterable { .heltecMeshNodeT114, .sensecapIndicator, .trackerT1000E, + .rak3172, + .wioE5, + .radiomaster900Bandit, .privateHw, ] -} -#endif // swift(>=4.2) +} /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum { +public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -617,26 +633,20 @@ public enum Constants: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] -} -#endif // swift(>=4.2) +} /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum { +public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -745,11 +755,6 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -767,13 +772,12 @@ extension CriticalErrorCode: CaseIterable { .flashCorruptionRecoverable, .flashCorruptionUnrecoverable, ] -} -#endif // swift(>=4.2) +} /// /// a gps position -public struct Position { +public struct Position: @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. @@ -990,7 +994,7 @@ public struct Position { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum { + public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1034,12 +1038,20 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] + } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum { + public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1089,6 +1101,15 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] + } public init() {} @@ -1096,31 +1117,6 @@ public struct Position { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension Position.LocSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] -} - -extension Position.AltSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] -} - -#endif // swift(>=4.2) - /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1142,7 +1138,7 @@ extension Position.AltSource: CaseIterable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User { +public struct User: @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. @@ -1167,6 +1163,8 @@ public struct User { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1198,7 +1196,7 @@ public struct User { /// /// A message used in our Dynamic Source Routing protocol (RFC 4728 based) -public struct RouteDiscovery { +public struct RouteDiscovery: 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. @@ -1214,7 +1212,7 @@ public struct RouteDiscovery { /// /// A Routing control Data packet handled by the routing module -public struct Routing { +public struct Routing: 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. @@ -1254,7 +1252,7 @@ public struct Routing { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1266,34 +1264,12 @@ public struct Routing { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) - #if !swift(>=4.1) - public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.routeRequest, .routeRequest): return { - guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.routeReply, .routeReply): return { - guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorReason, .errorReason): return { - guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum { + public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1345,6 +1321,10 @@ public struct Routing { /// The application layer service on the remote node received your request, but considered your request not authorized /// (i.e you did not send the request on the required bound channel) case notAuthorized // = 33 + + /// + /// The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all) + case pkiFailed // = 34 case UNRECOGNIZED(Int) public init() { @@ -1365,6 +1345,7 @@ public struct Routing { case 9: self = .dutyCycleLimit case 32: self = .badRequest case 33: self = .notAuthorized + case 34: self = .pkiFailed default: self = .UNRECOGNIZED(rawValue) } } @@ -1383,42 +1364,38 @@ public struct Routing { case .dutyCycleLimit: return 9 case .badRequest: return 32 case .notAuthorized: return 33 + case .pkiFailed: return 34 case .UNRECOGNIZED(let i): return i } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + .pkiFailed, + ] + } public init() {} } -#if swift(>=4.2) - -extension Routing.Error: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - ] -} - -#endif // swift(>=4.2) - /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage { +public struct DataMessage: @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. @@ -1472,7 +1449,7 @@ public struct DataMessage { /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint { +public struct Waypoint: 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. @@ -1534,7 +1511,7 @@ public struct Waypoint { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage { +public struct MqttClientProxyMessage: @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. @@ -1575,7 +1552,7 @@ public struct MqttClientProxyMessage { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// Bytes case data(Data) @@ -1583,24 +1560,6 @@ public struct MqttClientProxyMessage { /// Text case text(String) - #if !swift(>=4.1) - public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.data, .data): return { - guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -1610,7 +1569,7 @@ public struct MqttClientProxyMessage { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket { +public struct MeshPacket: @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. @@ -1744,6 +1703,8 @@ public struct MeshPacket { /// /// Describe if this message is delayed + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -1780,7 +1741,7 @@ public struct MeshPacket { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1788,24 +1749,6 @@ public struct MeshPacket { /// TODO: REPLACE case encrypted(Data) - #if !swift(>=4.1) - public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.decoded, .decoded): return { - guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.encrypted, .encrypted): return { - guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// @@ -1827,7 +1770,7 @@ public struct MeshPacket { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum { + public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1892,11 +1835,22 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .ack, + .max, + ] + } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum { + public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1934,6 +1888,13 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] + } public init() {} @@ -1941,32 +1902,6 @@ public struct MeshPacket { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension MeshPacket.Priority: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .ack, - .max, - ] -} - -extension MeshPacket.Delayed: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] -} - -#endif // swift(>=4.2) - /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -1984,7 +1919,7 @@ extension MeshPacket.Delayed: CaseIterable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo { +public struct NodeInfo: @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. @@ -2085,7 +2020,7 @@ public struct NodeInfo { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo { +public struct MyNodeInfo: 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. @@ -2116,7 +2051,7 @@ public struct MyNodeInfo { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord { +public struct LogRecord: 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. @@ -2141,7 +2076,7 @@ public struct LogRecord { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum { + public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2203,29 +2138,23 @@ public struct LogRecord { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] + } public init() {} } -#if swift(>=4.2) - -extension LogRecord.Level: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] -} - -#endif // swift(>=4.2) - -public struct QueueStatus { +public struct QueueStatus: 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. @@ -2252,7 +2181,7 @@ public struct QueueStatus { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio { +public struct FromRadio: 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. @@ -2428,7 +2357,7 @@ public struct FromRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2483,76 +2412,6 @@ public struct FromRadio { /// Notification message to the client case clientNotification(ClientNotification) - #if !swift(>=4.1) - public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.myInfo, .myInfo): return { - guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodeInfo, .nodeInfo): return { - guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.config, .config): return { - guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.logRecord, .logRecord): return { - guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.configCompleteID, .configCompleteID): return { - guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebooted, .rebooted): return { - guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.moduleConfig, .moduleConfig): return { - guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.channel, .channel): return { - guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.queueStatus, .queueStatus): return { - guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.metadata, .metadata): return { - guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileInfo, .fileInfo): return { - guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.clientNotification, .clientNotification): return { - guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2563,7 +2422,7 @@ public struct FromRadio { /// To be used for important messages that should to be displayed to the user /// in the form of push notifications or validation messages when saving /// invalid configuration. -public struct ClientNotification { +public struct ClientNotification: 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. @@ -2600,7 +2459,7 @@ public struct ClientNotification { /// /// Individual File info for the device -public struct FileInfo { +public struct FileInfo: 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. @@ -2621,7 +2480,7 @@ public struct FileInfo { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio { +public struct ToRadio: 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. @@ -2701,7 +2560,7 @@ public struct ToRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -2728,40 +2587,6 @@ public struct ToRadio { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) - #if !swift(>=4.1) - public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.wantConfigID, .wantConfigID): return { - guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.disconnect, .disconnect): return { - guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2769,7 +2594,7 @@ public struct ToRadio { /// /// Compressed message payload -public struct Compressed { +public struct Compressed: @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. @@ -2789,7 +2614,7 @@ public struct Compressed { /// /// Full info on edges for a single node -public struct NeighborInfo { +public struct NeighborInfo: 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. @@ -2817,7 +2642,7 @@ public struct NeighborInfo { /// /// A single edge in the mesh -public struct Neighbor { +public struct Neighbor: 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. @@ -2847,7 +2672,7 @@ public struct Neighbor { /// /// Device metadata response -public struct DeviceMetadata { +public struct DeviceMetadata: 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. @@ -2900,7 +2725,7 @@ public struct DeviceMetadata { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat { +public struct Heartbeat: 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. @@ -2912,7 +2737,7 @@ public struct Heartbeat { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin { +public struct NodeRemoteHardwarePin: 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. @@ -2939,7 +2764,7 @@ public struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload { +public struct ChunkedPayload: @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. @@ -2967,7 +2792,7 @@ public struct ChunkedPayload { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks { +public struct resend_chunks: 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. @@ -2981,7 +2806,7 @@ public struct resend_chunks { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse { +public struct ChunkedPayloadResponse: 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. @@ -3024,7 +2849,7 @@ public struct ChunkedPayloadResponse { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -3035,76 +2860,11 @@ public struct ChunkedPayloadResponse { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) - #if !swift(>=4.1) - public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.requestTransfer, .requestTransfer): return { - guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.acceptTransfer, .acceptTransfer): return { - guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.resendChunks, .resendChunks): return { - guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareModel: @unchecked Sendable {} -extension Constants: @unchecked Sendable {} -extension CriticalErrorCode: @unchecked Sendable {} -extension Position: @unchecked Sendable {} -extension Position.LocSource: @unchecked Sendable {} -extension Position.AltSource: @unchecked Sendable {} -extension User: @unchecked Sendable {} -extension RouteDiscovery: @unchecked Sendable {} -extension Routing: @unchecked Sendable {} -extension Routing.OneOf_Variant: @unchecked Sendable {} -extension Routing.Error: @unchecked Sendable {} -extension DataMessage: @unchecked Sendable {} -extension Waypoint: @unchecked Sendable {} -extension MqttClientProxyMessage: @unchecked Sendable {} -extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket: @unchecked Sendable {} -extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket.Priority: @unchecked Sendable {} -extension MeshPacket.Delayed: @unchecked Sendable {} -extension NodeInfo: @unchecked Sendable {} -extension MyNodeInfo: @unchecked Sendable {} -extension LogRecord: @unchecked Sendable {} -extension LogRecord.Level: @unchecked Sendable {} -extension QueueStatus: @unchecked Sendable {} -extension FromRadio: @unchecked Sendable {} -extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension ClientNotification: @unchecked Sendable {} -extension FileInfo: @unchecked Sendable {} -extension ToRadio: @unchecked Sendable {} -extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension Compressed: @unchecked Sendable {} -extension NeighborInfo: @unchecked Sendable {} -extension Neighbor: @unchecked Sendable {} -extension DeviceMetadata: @unchecked Sendable {} -extension Heartbeat: @unchecked Sendable {} -extension NodeRemoteHardwarePin: @unchecked Sendable {} -extension ChunkedPayload: @unchecked Sendable {} -extension resend_chunks: @unchecked Sendable {} -extension ChunkedPayloadResponse: @unchecked Sendable {} -extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -3182,6 +2942,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 69: .same(proto: "HELTEC_MESH_NODE_T114"), 70: .same(proto: "SENSECAP_INDICATOR"), 71: .same(proto: "TRACKER_T1000_E"), + 72: .same(proto: "RAK3172"), + 73: .same(proto: "WIO_E5"), + 74: .same(proto: "RADIOMASTER_900_BANDIT"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3686,6 +3449,7 @@ extension Routing.Error: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "DUTY_CYCLE_LIMIT"), 32: .same(proto: "BAD_REQUEST"), 33: .same(proto: "NOT_AUTHORIZED"), + 34: .same(proto: "PKI_FAILED"), ] } @@ -4074,7 +3838,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr != 0 { + if _storage._rxSnr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -4257,7 +4021,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -5102,7 +4866,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr != 0 { + if self.snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -5215,8 +4979,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index c68ffd83..6f3b2d76 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +58,18 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] -} -#endif // swift(>=4.2) +} /// /// Module Config -public struct ModuleConfig { +public struct ModuleConfig: 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. @@ -218,7 +212,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -259,73 +253,11 @@ public struct ModuleConfig { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) - #if !swift(>=4.1) - public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.mqtt, .mqtt): return { - guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.serial, .serial): return { - guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.externalNotification, .externalNotification): return { - guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeForward, .storeForward): return { - guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rangeTest, .rangeTest): return { - guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.telemetry, .telemetry): return { - guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.cannedMessage, .cannedMessage): return { - guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.audio, .audio): return { - guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.remoteHardware, .remoteHardware): return { - guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.neighborInfo, .neighborInfo): return { - guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.ambientLighting, .ambientLighting): return { - guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detectionSensor, .detectionSensor): return { - guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.paxcounter, .paxcounter): return { - guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// MQTT Client Config - public struct MQTTConfig { + public struct MQTTConfig: 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. @@ -400,7 +332,7 @@ public struct ModuleConfig { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings { + public struct MapReportSettings: 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. @@ -420,7 +352,7 @@ public struct ModuleConfig { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig { + public struct RemoteHardwareConfig: 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. @@ -444,7 +376,7 @@ public struct ModuleConfig { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig { + public struct NeighborInfoConfig: 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. @@ -465,7 +397,7 @@ public struct ModuleConfig { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig { + public struct DetectionSensorConfig: 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. @@ -516,7 +448,7 @@ public struct ModuleConfig { /// /// Audio Config for codec2 voice - public struct AudioConfig { + public struct AudioConfig: 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. @@ -553,7 +485,7 @@ public struct ModuleConfig { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum { + public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -600,6 +532,19 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] + } public init() {} @@ -607,7 +552,7 @@ public struct ModuleConfig { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig { + public struct PaxcounterConfig: 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. @@ -633,7 +578,7 @@ public struct ModuleConfig { /// /// Serial Config - public struct SerialConfig { + public struct SerialConfig: 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. @@ -676,7 +621,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum { + public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -744,11 +689,31 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] + } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum { + public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -793,6 +758,17 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + ] + } public init() {} @@ -800,7 +776,7 @@ public struct ModuleConfig { /// /// External Notifications Config - public struct ExternalNotificationConfig { + public struct ExternalNotificationConfig: 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. @@ -883,7 +859,7 @@ public struct ModuleConfig { /// /// Store and Forward Module Config - public struct StoreForwardConfig { + public struct StoreForwardConfig: 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. @@ -919,7 +895,7 @@ public struct ModuleConfig { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig { + public struct RangeTestConfig: 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. @@ -944,7 +920,7 @@ public struct ModuleConfig { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig { + public struct TelemetryConfig: 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. @@ -1001,7 +977,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public struct CannedMessageConfig { + public struct CannedMessageConfig: 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. @@ -1056,7 +1032,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1124,6 +1100,18 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] + } public init() {} @@ -1132,7 +1120,7 @@ public struct ModuleConfig { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig { + public struct AmbientLightingConfig: 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. @@ -1165,77 +1153,9 @@ public struct ModuleConfig { public init() {} } -#if swift(>=4.2) - -extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] -} - -extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] -} - -#endif // swift(>=4.2) - /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin { +public struct RemoteHardwarePin: 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. @@ -1257,31 +1177,6 @@ public struct RemoteHardwarePin { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RemoteHardwarePinType: @unchecked Sendable {} -extension ModuleConfig: @unchecked Sendable {} -extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} -extension ModuleConfig.MQTTConfig: @unchecked Sendable {} -extension ModuleConfig.MapReportSettings: @unchecked Sendable {} -extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} -extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} -extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} -extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} -extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} -extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} -extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} -extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} -extension RemoteHardwarePin: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index efe6cdd5..fc5e37a1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope { +public struct ServiceEnvelope: 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport { +public struct MapReport: 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. @@ -121,11 +121,6 @@ public struct MapReport { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension ServiceEnvelope: @unchecked Sendable {} -extension MapReport: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index cf8aa463..f82b3c51 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount { +public struct Paxcount: 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. @@ -44,10 +44,6 @@ public struct Paxcount { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Paxcount: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index c728c961..c5348a8a 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum { +public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -277,11 +277,6 @@ public enum PortNum: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -313,14 +308,9 @@ extension PortNum: CaseIterable { .atakForwarder, .max, ] + } -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PortNum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 5f51e948..9c61e6d0 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon { +public struct PowerMon: 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. @@ -31,7 +31,7 @@ public struct PowerMon { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum { + public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,37 +104,31 @@ public struct PowerMon { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerMon.State: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] -} - -#endif // swift(>=4.2) - /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage { +public struct PowerStressMessage: 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. @@ -151,7 +145,7 @@ public struct PowerStressMessage { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum { + public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -272,48 +266,35 @@ public struct PowerStressMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerStressMessage.Opcode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PowerMon: @unchecked Sendable {} -extension PowerMon.State: @unchecked Sendable {} -extension PowerStressMessage: @unchecked Sendable {} -extension PowerStressMessage.Opcode: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -323,8 +304,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -379,7 +360,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds != 0 { + if self.numSeconds.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index ac6eeb26..60f64504 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage { +public struct HardwareMessage: 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. @@ -52,7 +52,7 @@ public struct HardwareMessage { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum { + public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -110,32 +110,21 @@ public struct HardwareMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] + } public init() {} } -#if swift(>=4.2) - -extension HardwareMessage.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareMessage: @unchecked Sendable {} -extension HardwareMessage.TypeEnum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index 6fdf3208..c1f3f678 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig { +public struct RTTTLConfig: 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. @@ -36,10 +36,6 @@ public struct RTTTLConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RTTTLConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 54efa77b..0b67eaf6 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward { +public struct StoreAndForward: @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. @@ -79,7 +79,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,38 +93,12 @@ public struct StoreAndForward { /// Text from history message. case text(Data) - #if !swift(>=4.1) - public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.stats, .stats): return { - guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.history, .history): return { - guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum { + public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -242,11 +216,31 @@ public struct StoreAndForward { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] + } /// /// TODO: REPLACE - public struct Statistics { + public struct Statistics: 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. @@ -294,7 +288,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct History { + public struct History: 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. @@ -319,7 +313,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct Heartbeat { + public struct Heartbeat: 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. @@ -340,41 +334,6 @@ public struct StoreAndForward { public init() {} } -#if swift(>=4.2) - -extension StoreAndForward.RequestResponse: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension StoreAndForward: @unchecked Sendable {} -extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} -extension StoreAndForward.RequestResponse: @unchecked Sendable {} -extension StoreAndForward.Statistics: @unchecked Sendable {} -extension StoreAndForward.History: @unchecked Sendable {} -extension StoreAndForward.Heartbeat: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index de4e550c..7289b713 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum { +public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -128,6 +128,18 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// NAU7802 Scale Chip or compatible case nau7802 // = 25 + + /// + /// BMP3XX High accuracy temperature and pressure + case bmp3Xx // = 26 + + /// + /// ICM-20948 9-Axis digital motion processor + case icm20948 // = 27 + + /// + /// MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) + case max17048 // = 28 case UNRECOGNIZED(Int) public init() { @@ -162,6 +174,9 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 23: self = .aht10 case 24: self = .dfrobotLark case 25: self = .nau7802 + case 26: self = .bmp3Xx + case 27: self = .icm20948 + case 28: self = .max17048 default: self = .UNRECOGNIZED(rawValue) } } @@ -194,15 +209,13 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .aht10: return 23 case .dfrobotLark: return 24 case .nau7802: return 25 + case .bmp3Xx: return 26 + case .icm20948: return 27 + case .max17048: return 28 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -231,14 +244,16 @@ extension TelemetrySensorType: CaseIterable { .aht10, .dfrobotLark, .nau7802, + .bmp3Xx, + .icm20948, + .max17048, ] -} -#endif // swift(>=4.2) +} /// /// Key native device metrics such as battery level -public struct DeviceMetrics { +public struct DeviceMetrics: 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. @@ -311,7 +326,7 @@ public struct DeviceMetrics { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics { +public struct EnvironmentMetrics: @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. @@ -514,7 +529,7 @@ public struct EnvironmentMetrics { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics { +public struct PowerMetrics: 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. @@ -599,7 +614,7 @@ public struct PowerMetrics { /// /// Air quality metrics -public struct AirQualityMetrics { +public struct AirQualityMetrics: 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. @@ -756,7 +771,7 @@ public struct AirQualityMetrics { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry { +public struct Telemetry: 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. @@ -809,7 +824,7 @@ public struct Telemetry { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -823,32 +838,6 @@ public struct Telemetry { /// Power Metrics case powerMetrics(PowerMetrics) - #if !swift(>=4.1) - public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.deviceMetrics, .deviceMetrics): return { - guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.environmentMetrics, .environmentMetrics): return { - guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.airQualityMetrics, .airQualityMetrics): return { - guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.powerMetrics, .powerMetrics): return { - guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -856,7 +845,7 @@ public struct Telemetry { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config { +public struct Nau7802Config: 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. @@ -874,17 +863,6 @@ public struct Nau7802Config { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension TelemetrySensorType: @unchecked Sendable {} -extension DeviceMetrics: @unchecked Sendable {} -extension EnvironmentMetrics: @unchecked Sendable {} -extension PowerMetrics: @unchecked Sendable {} -extension AirQualityMetrics: @unchecked Sendable {} -extension Telemetry: @unchecked Sendable {} -extension Telemetry.OneOf_Variant: @unchecked Sendable {} -extension Nau7802Config: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -917,6 +895,9 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 23: .same(proto: "AHT10"), 24: .same(proto: "DFROBOT_LARK"), 25: .same(proto: "NAU7802"), + 26: .same(proto: "BMP3XX"), + 27: .same(proto: "ICM20948"), + 28: .same(proto: "MAX17048"), ] } @@ -1488,7 +1469,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor != 0 { + if self.calibrationFactor.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 1f41fe0b..89d0097c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem { +public struct XModem: @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. @@ -35,7 +35,7 @@ public struct XModem { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum { + public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,34 +79,23 @@ public struct XModem { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] + } public init() {} } -#if swift(>=4.2) - -extension XModem.Control: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension XModem: @unchecked Sendable {} -extension XModem.Control: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/protobufs b/protobufs index 3e753697..97fa3451 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 3e753697aa1140d2c998cb63739729e733002874 +Subproject commit 97fa34517f80332a11046a73f26d55100fbee9e2 From 7b3553bbf168acc556a8c6c3ca25a2f06d96c953 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 10:52:59 -0700 Subject: [PATCH 096/333] Add new error --- Meshtastic/Enums/RoutingError.swift | 6 +++++- protobufs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 0773265b..50de17a5 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -21,6 +21,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case dutyCycleLimit = 9 case badRequest = 32 case notAuthorized = 33 + case pkiFailed = 34 var id: Int { self.rawValue } var display: String { @@ -50,6 +51,8 @@ enum RoutingError: Int, CaseIterable, Identifiable { return "routing.badRequest".localized case .notAuthorized: return "routing.notauthorized".localized + case .pkiFailed: + return "routing.pkiFailed".localized } } func protoEnumValue() -> Routing.Error { @@ -80,7 +83,8 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Routing.Error.badRequest case .notAuthorized: return Routing.Error.notAuthorized - + case .pkiFailed: + return Routing.Error.pkiFailed } } } diff --git a/protobufs b/protobufs index 97fa3451..3e753697 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 97fa34517f80332a11046a73f26d55100fbee9e2 +Subproject commit 3e753697aa1140d2c998cb63739729e733002874 From 7a4ae7957da013611643730e26dfb1394aff6952 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 11:08:22 -0700 Subject: [PATCH 097/333] translation value for ack error --- Localizable.xcstrings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 6160d6ae..8f9fe742 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -18440,6 +18440,17 @@ } } }, + "routing.pkifailed" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKI Failed" + } + } + } + }, "routing.timeout" : { "extractionState" : "migrated", "localizations" : { From 59a104d9e97b8f780fa20dc55c237d0075ec5b15 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 14:11:08 -0700 Subject: [PATCH 098/333] ack! --- Localizable.xcstrings | 22 +++++++---- Meshtastic/Enums/RoutingError.swift | 39 +++++++++++++++++++ Meshtastic/Resources/DeviceHardware.json | 2 +- Meshtastic/Tips/MessagesTips.swift | 2 +- .../Views/Messages/ChannelMessageList.swift | 8 ++-- Meshtastic/Views/Messages/UserList.swift | 2 +- .../Views/Messages/UserMessageList.swift | 6 ++- 7 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 8f9fe742..1656a3f4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -334,9 +334,6 @@ }, "Ack Time: %@" : { - }, - "Acknowledged" : { - }, "Acknowledged by another node" : { @@ -6700,9 +6697,6 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { - }, - "Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation." : { - }, "echo" : { "localizations" : { @@ -7209,6 +7203,9 @@ }, "Favorites" : { + }, + "Favorites and nodes with recent messages show up at the top of the list. Contacts using the shared key display an open lock, nodes with a private key show a green lock and a red key with a slash will show up if a key has changed for a contact. Long press to favorite or mute the contact or delete a conversation." : { + }, "Fifteen Minutes" : { @@ -18446,7 +18443,18 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "PKI Failed" + "value" : "Encrypted Send Failed" + } + } + } + }, + "routing.pkiunknownpubkey" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown Public Key" } } } diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 50de17a5..6dc83692 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -5,6 +5,7 @@ // Copyright(c) Garth Vander Houwen 8/4/22. // import Foundation +import SwiftUI import MeshtasticProtobufs enum RoutingError: Int, CaseIterable, Identifiable { @@ -22,6 +23,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case badRequest = 32 case notAuthorized = 33 case pkiFailed = 34 + case pkiUnknownPubkey = 35 var id: Int { self.rawValue } var display: String { @@ -53,6 +55,41 @@ enum RoutingError: Int, CaseIterable, Identifiable { return "routing.notauthorized".localized case .pkiFailed: return "routing.pkiFailed".localized + case .pkiUnknownPubkey: + return "routing.pkiunknownpubkey".localized + } + } + var color: Color { + switch self { + + case .none: + return Color.secondary + case .noRoute: + return Color.red + case .gotNak: + return Color.red + case .timeout: + return Color.orange + case .noInterface: + return Color.red + case .maxRetransmit: + return Color.red + case .noChannel: + return Color.orange + case .tooLarge: + return Color.red + case .noResponse: + return Color.orange + case .dutyCycleLimit: + return Color.orange + case .badRequest: + return Color.red + case .notAuthorized: + return Color.red + case .pkiFailed: + return Color.red + case .pkiUnknownPubkey: + return Color.red } } func protoEnumValue() -> Routing.Error { @@ -85,6 +122,8 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Routing.Error.notAuthorized case .pkiFailed: return Routing.Error.pkiFailed + case .pkiUnknownPubkey: + return Routing.Error.pkiFailed } } } diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 09eb2e6c..cbad8df9 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -370,7 +370,7 @@ { "hwModel": 66, "hwModelSlug": "HELTEC_VISION_MASTER_T190", - "platformioTarget": "heltec-vision-master-T190", + "platformioTarget": "heltec-vision-master-t190", "architecture": "esp32-s3", "activelySupported": true, "displayName": "Heltec Vision Master T190" diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index d78daa0e..5bd50d54 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -38,7 +38,7 @@ struct ContactsTip: Tip { } var message: Text? { // Text("tip.messages.contacts.message") - Text("Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation.") + Text("Favorites and nodes with recent messages show up at the top of the list. Contacts using the shared key display an open lock, nodes with a private key show a green lock and a red key with a slash will show up if a key has changed for a contact. Long press to favorite or mute the contact or delete a conversation.") } var image: Image? { Image(systemName: "person.circle") diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index b996e790..95b727f0 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -84,16 +84,14 @@ struct ChannelMessageList: View { } HStack { - if currentUser && message.receivedACK { - // Ack Received - Text("Acknowledged").font(.caption2).foregroundColor(.gray) - } else if currentUser && message.ackError == 0 { + if currentUser && message.ackError == 0 { // Empty Error Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) } else if currentUser && message.ackError > 0 { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) + .foregroundStyle(ackErrorVal?.color ?? .red) + .font(.caption2) } else if isDetectionSensorMessage { let messageDate = message.timestamp Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4d7b2fd9..96dd398e 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -185,7 +185,7 @@ struct UserList: View { } } .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1))) + .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count))) .sheet(isPresented: $isEditingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 914fde3f..977c1686 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -78,7 +78,9 @@ struct UserMessageList: View { if currentUser && message.receivedACK { // Ack Received if message.realACK { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").font(.caption2).foregroundColor(.gray) + Text("\(ackErrorVal?.display ?? "Empty Ack Error")") + .font(.caption2) + .foregroundStyle(ackErrorVal?.color ?? Color.secondary) } else { Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange) } @@ -87,7 +89,7 @@ struct UserMessageList: View { Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow) } else if currentUser && message.ackError > 0 { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) + .foregroundStyle(ackErrorVal?.color ?? Color.red) } } } From 761f73da7700d75dfb36f26ef4865927859fb282 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Aug 2024 16:22:40 -0500 Subject: [PATCH 099/333] 2.5 update protos --- MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift | 8 ++++++++ protobufs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index c9482fc1..2aa80dc1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1325,6 +1325,10 @@ public struct Routing: Sendable { /// /// The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all) case pkiFailed // = 34 + + /// + /// The receiving node does not have a Public Key to decode with + case pkiUnknownPubkey // = 35 case UNRECOGNIZED(Int) public init() { @@ -1346,6 +1350,7 @@ public struct Routing: Sendable { case 32: self = .badRequest case 33: self = .notAuthorized case 34: self = .pkiFailed + case 35: self = .pkiUnknownPubkey default: self = .UNRECOGNIZED(rawValue) } } @@ -1365,6 +1370,7 @@ public struct Routing: Sendable { case .badRequest: return 32 case .notAuthorized: return 33 case .pkiFailed: return 34 + case .pkiUnknownPubkey: return 35 case .UNRECOGNIZED(let i): return i } } @@ -1384,6 +1390,7 @@ public struct Routing: Sendable { .badRequest, .notAuthorized, .pkiFailed, + .pkiUnknownPubkey, ] } @@ -3450,6 +3457,7 @@ extension Routing.Error: SwiftProtobuf._ProtoNameProviding { 32: .same(proto: "BAD_REQUEST"), 33: .same(proto: "NOT_AUTHORIZED"), 34: .same(proto: "PKI_FAILED"), + 35: .same(proto: "PKI_UNKNOWN_PUBKEY"), ] } diff --git a/protobufs b/protobufs index 3e753697..8b5b2faf 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 3e753697aa1140d2c998cb63739729e733002874 +Subproject commit 8b5b2faf662b364754809f923271022f4f1492ed From b3301bac7a2f896bb0eb8b19f60f4fc67f1dc259 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 14:57:34 -0700 Subject: [PATCH 100/333] turn on key match warning on receipt of a pki failed error --- Meshtastic/Helpers/MeshPackets.swift | 3 +++ protobufs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 96db5fd2..44538947 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -628,6 +628,9 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana } } fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) + if routingError == RoutingError.pkiFailed { + fetchedMessage[0].toUser?.keyMatch = false + } if routingMessage.errorReason == Routing.Error.none { diff --git a/protobufs b/protobufs index 8b5b2faf..3e753697 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 8b5b2faf662b364754809f923271022f4f1492ed +Subproject commit 3e753697aa1140d2c998cb63739729e733002874 From c48f96b2b43bd101df45badcdfc75afbbd469873 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 15:00:20 -0700 Subject: [PATCH 101/333] Add unknown pubkey --- Meshtastic/Enums/RoutingError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 6dc83692..4bdb8388 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -123,7 +123,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .pkiFailed: return Routing.Error.pkiFailed case .pkiUnknownPubkey: - return Routing.Error.pkiFailed + return Routing.Error.pkiUnknownPubkey } } } From c77da6b119bfb1e1f94e5806f14879700f8d5479 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Aug 2024 19:02:43 -0500 Subject: [PATCH 102/333] Short turbo preset --- Meshtastic/Enums/LoraConfigEnums.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 7da6268e..b0dd9966 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -210,6 +210,7 @@ enum ModemPresets: Int, CaseIterable, Identifiable { case medFast = 4 case shortSlow = 5 case shortFast = 6 + case shortTurbo = 8 var id: Int { self.rawValue } var description: String { @@ -230,6 +231,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "Short Range - Slow" case .shortFast: return "Short Range - Fast" + case .shortTurbo: + return "Short Range - Turbo" } } var name: String { @@ -250,6 +253,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "ShortSlow" case .shortFast: return "ShortFast" + case .shortTurbo: + return "ShortTurbo" } } func snrLimit() -> Float { @@ -270,6 +275,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return -10 case .shortFast: return -7.5 + case .shortTurbo: + return -7.5 } } func protoEnumValue() -> Config.LoRaConfig.ModemPreset { @@ -290,6 +297,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return Config.LoRaConfig.ModemPreset.shortSlow case .shortFast: return Config.LoRaConfig.ModemPreset.shortFast + case .shortTurbo: + return Config.LoRaConfig.ModemPreset.shortTurbo } } } From 8c3b4ada3781f7ad183ca94756801948d6989cda Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 17:04:48 -0700 Subject: [PATCH 103/333] Move the lock to the message corner --- Meshtastic/Views/Messages/MessageText.swift | 16 +++++++++++++++- Meshtastic/Views/Messages/UserMessageList.swift | 6 ------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 0cdd4be8..57adc99c 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -24,11 +24,25 @@ struct MessageText: View { let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) return Text(markdownText) .tint(Self.linkBlue) - .padding(10) + .padding(.vertical, 10) + .padding(.horizontal, 8) .foregroundColor(.white) .background(isCurrentUser ? .accentColor : Color(.gray)) .cornerRadius(15) .overlay { + if message.pkiEncrypted { + VStack (alignment: .trailing) { + Spacer() + HStack { + Spacer() + Image(systemName: "lock.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .green) + .font(.system(size: 20)) + .offset(x: 8, y: 8) + } + } + } let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) if tapBackDestination.overlaySensorMessage { VStack { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 977c1686..b0ec3a10 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -57,12 +57,6 @@ struct UserMessageList: View { self.replyMessageId = message.messageId self.messageFieldFocused = true } - if message.pkiEncrypted { - Image(systemName: "lock.circle.fill") - .foregroundStyle(.green) - .frame(height: 25) - .padding(.top, 5) - } if currentUser && message.canRetry || (message.receivedACK && !message.realACK) { RetryButton(message: message, destination: .user(user)) From 3b0d40668f8438ba3f72e743ca542cf3dd867c9a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 17:09:05 -0700 Subject: [PATCH 104/333] Remove public key from context menu --- Meshtastic/Views/Messages/MessageContextMenuItems.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 07792a9a..c9c37cdc 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -15,10 +15,6 @@ struct MessageContextMenuItems: View { VStack { if message.pkiEncrypted { Label("Encrypted", systemImage: "lock") - Text("Public Key") - Text(message.publicKey?.base64EncodedString() ?? "No Key") - .allowsTightening(true) - .monospaced() } Text("channel") + Text(": \(message.channel)") } From 0b5e72cd43fb930343dd173dbff46fcf7cd4fb76 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 20:52:47 -0700 Subject: [PATCH 105/333] Public key mismatch warning --- Localizable.xcstrings | 6 ++++++ Meshtastic/Helpers/MeshPackets.swift | 2 ++ .../contents | 1 + .../Views/Nodes/Helpers/NodeDetail.swift | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1656a3f4..11662cbc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -10784,6 +10784,9 @@ }, "Key" : { + }, + "Key does not match the public key that was used previously, delete the node and let it negotatiate keys again." : { + }, "Key Mapping" : { @@ -16659,6 +16662,9 @@ }, "Public Key" : { + }, + "Public Key Mismatch" : { + }, "PWD" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 44538947..952899dd 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -630,6 +630,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) if routingError == RoutingError.pkiFailed { fetchedMessage[0].toUser?.keyMatch = false + fetchedMessage[0].toUser?.newPublicKey = fetchedMessage[0].publicKey } if routingMessage.errorReason == Routing.Error.none { @@ -854,6 +855,7 @@ func textMessageAppPacket( /// We have a key, check if it matches if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false + newMessage.fromUser?.newPublicKey = newMessage.publicKey } } else { /// We have no key, set it diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index b92f33f6..ac5d631b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -436,6 +436,7 @@ + diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 68769280..0d9897c7 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -44,6 +44,24 @@ struct NodeDetail: View { NodeInfoItem(node: node) } Section("Node") { + if let user = node.user { + if user.keyMatch { + Label { + VStack(alignment: .leading) { + Text("Public Key Mismatch") + .font(.title3) + .foregroundStyle(.red) + Text("Key does not match the public key that was used previously, delete the node and let it negotatiate keys again.") + .font(.caption) + .foregroundStyle(.red) + } + } icon: { + Image(systemName: "key.slash.fill") + .symbolRenderingMode(.multicolor) + .foregroundStyle(.red) + } + } + } HStack { Label { Text("Node Number") From 197b567db0c7ece2a29eaa9233a28619645fd7fc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 21:32:17 -0700 Subject: [PATCH 106/333] Clean up the filters --- Meshtastic/Views/Messages/UserList.swift | 20 ++++++++++---------- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 96dd398e..d4bfdbf1 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -30,6 +30,15 @@ struct UserList: View { @State private var deviceRoles: Set = [] @State var isEditingFilters = false @State private var showingTrustConfirm: Bool = false + + var boolFilters: [Bool] {[ + isFavorite, + isOnline, + isPkiEncrypted, + isEnvironment, + distanceFilter, + roleFilter + ]} @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), @@ -210,21 +219,12 @@ struct UserList: View { .onChange(of: hopsAway) { _ in searchUserList() } - .onChange(of: isOnline) { _ in - searchUserList() - } - .onChange(of: isPkiEncrypted) { _ in - searchUserList() - } - .onChange(of: isFavorite) { _ in + .onChange(of: [boolFilters]) { _ in searchUserList() } .onChange(of: maxDistance) { _ in searchUserList() } - .onChange(of: distanceFilter) { _ in - searchUserList() - } .onAppear { searchUserList() } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index f85cad48..310abc9b 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -302,7 +302,7 @@ struct NodeList: View { await searchNodeList() } } - .onChange(of: boolFilters) { _ in + .onChange(of: [boolFilters]) { _ in Task { await searchNodeList() } From 5c8eea9ea8d72006e5d452d0f7ad45e101a9506b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 14 Aug 2024 21:36:31 -0700 Subject: [PATCH 107/333] Key match warning search on first appear --- Meshtastic/Views/Messages/UserList.swift | 2 +- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index d4bfdbf1..cdbf291a 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -225,7 +225,7 @@ struct UserList: View { .onChange(of: maxDistance) { _ in searchUserList() } - .onAppear { + .onFirstAppear { searchUserList() } .safeAreaInset(edge: .bottom, alignment: .trailing) { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 0d9897c7..e8eeeecd 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -45,7 +45,7 @@ struct NodeDetail: View { } Section("Node") { if let user = node.user { - if user.keyMatch { + if !user.keyMatch { Label { VStack(alignment: .leading) { Text("Public Key Mismatch") diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 310abc9b..5248b6bf 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -337,7 +337,7 @@ struct NodeList: View { self.selectedNode = nil } } - .onAppear { + .onFirstAppear { Task { await searchNodeList() } From fd38f96dc7494ff8643bb0d09c0d83fa4cf4d1fc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 07:20:45 -0700 Subject: [PATCH 108/333] Bump version, set current timestamp for packets with a 0 timestamp --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Helpers/MeshPackets.swift | 6 +++++- Meshtastic/Views/Messages/MessageText.swift | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c891cca9..2e083bd6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1629,7 +1629,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1664,7 +1664,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1696,7 +1696,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1729,7 +1729,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 952899dd..42f3b2ca 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -826,7 +826,11 @@ func textMessageAppPacket( let fetchedUsers = try context.fetch(messageUsers) let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) - newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + if packet.rxTime == 0 { + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) + } else { + newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + } newMessage.receivedACK = false newMessage.snr = packet.rxSnr newMessage.rssi = packet.rxRssi diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 57adc99c..93f8cf25 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -31,7 +31,7 @@ struct MessageText: View { .cornerRadius(15) .overlay { if message.pkiEncrypted { - VStack (alignment: .trailing) { + VStack(alignment: .trailing) { Spacer() HStack { Spacer() From 72b7cc3e3c332f336b1ad55a2fa1a313e4130924 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 08:15:11 -0700 Subject: [PATCH 109/333] Node list message user context menu item --- Localizable.xcstrings | 3 +++ Meshtastic/Views/Nodes/NodeList.swift | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 11662cbc..785091d8 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -14524,6 +14524,9 @@ } } } + }, + "Message" : { + }, "message.details" : { "localizations" : { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 5248b6bf..e252aa95 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -88,8 +88,15 @@ struct NodeList: View { context: context, node: node ) - /// Don't show trace route, position exchange or delete context menu items for the connected node + /// Don't show message, trace route, position exchange or delete context menu items for the connected node if connectedNode.num != node.num { + Button(action: { + if let url = URL(string: "meshtastic:///messages?userNum=\(node.num)") { + UIApplication.shared.open(url) + } + }) { + Label("Message", systemImage: "message") + } Button { let traceRouteSent = bleManager.sendTraceRouteRequest( destNum: node.num, From 2415ef28af09dc543596cdcc6a19b1ac7df20476 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 13:43:31 -0700 Subject: [PATCH 110/333] Update some last heard dates if they aer 0 --- Meshtastic/Helpers/MeshPackets.swift | 18 +++++++++++++++--- Meshtastic/Persistence/UpdateCoreData.swift | 10 +++++++--- Meshtastic/Views/Messages/UserList.swift | 1 + .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 42f3b2ca..3a398ccf 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -638,7 +638,12 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana fetchedMessage[0].receivedACK = true } fetchedMessage[0].ackSNR = packet.rxSnr - fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + + if packet.rxTime == 0 { + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) + } else { + fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + } if fetchedMessage[0].toUser != nil { fetchedMessage[0].toUser!.objectWillChange.send() @@ -727,7 +732,12 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage return } mutableTelemetries.add(telemetry) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(truncatingIfNeeded: packet.rxTime))) + if packet.rxTime == 0 { + fetchedNode[0].lastHeard = Date() + } else { + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(packet.rxTime)) + } + fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } try context.save() @@ -866,7 +876,9 @@ func textMessageAppPacket( newMessage.fromUser?.publicKey = packet.publicKey newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted } - if packet.rxTime > 0 { + if packet.rxTime == 0 { + newMessage.fromUser?.userNode?.lastHeard = Date() + } else { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index d52131b4..ba391495 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -148,6 +148,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if packet.rxTime > 0 { newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() } newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi @@ -233,9 +236,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].num = Int64(packet.from) if packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - if fetchedNode[0].firstHeard == nil { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - } + } else { + fetchedNode[0].lastHeard = Date() } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi @@ -363,6 +365,8 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } else if packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + fetchedNode[0].lastHeard = Date() } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index cdbf291a..5294c752 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -96,6 +96,7 @@ struct UserList: View { } Text(user.longName ?? "unknown".localized) .font(.headline) + .allowsTightening(true) Spacer() if user.userNode?.favorite ?? false { Image(systemName: "star.fill") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 10829f31..5d46806c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -29,8 +29,8 @@ struct NodeListItem: View { VStack(alignment: .leading) { HStack { Text(node.user?.longName ?? "unknown".localized) - .fontWeight(.medium) .font(.headline) + .allowsTightening(true) if node.favorite { Spacer() Image(systemName: "star.fill") From 7e80accfad79cf6500d77b8cb7a8633b0cf7baa7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 13:55:46 -0700 Subject: [PATCH 111/333] use > 0 for all empy rx times --- Meshtastic/Helpers/MeshPackets.swift | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3a398ccf..87ada50b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -638,11 +638,10 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana fetchedMessage[0].receivedACK = true } fetchedMessage[0].ackSNR = packet.rxSnr - - if packet.rxTime == 0 { - fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) - } else { + if packet.rxTime > 0 { fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + } else { + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) } if fetchedMessage[0].toUser != nil { @@ -732,12 +731,11 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage return } mutableTelemetries.add(telemetry) - if packet.rxTime == 0 { - fetchedNode[0].lastHeard = Date() - } else { + if packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(packet.rxTime)) + } else { + fetchedNode[0].lastHeard = Date() } - fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } try context.save() @@ -836,10 +834,10 @@ func textMessageAppPacket( let fetchedUsers = try context.fetch(messageUsers) let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) - if packet.rxTime == 0 { - newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) - } else { + if packet.rxTime > 0 { newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + } else { + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) } newMessage.receivedACK = false newMessage.snr = packet.rxSnr @@ -876,10 +874,10 @@ func textMessageAppPacket( newMessage.fromUser?.publicKey = packet.publicKey newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted } - if packet.rxTime == 0 { - newMessage.fromUser?.userNode?.lastHeard = Date() - } else { + if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + newMessage.fromUser?.userNode?.lastHeard = Date() } } newMessage.messagePayload = messageText From ce3aacba6374e4ac83495f7c87fb50e8ac20909e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 16:31:58 -0700 Subject: [PATCH 112/333] Help! --- Localizable.xcstrings | 35 ++++++++- Meshtastic.xcodeproj/project.pbxproj | 20 +++++ Meshtastic/Enums/RoutingError.swift | 4 +- Meshtastic/Tips/MessagesTips.swift | 19 ----- Meshtastic/Views/Helpers/Help/AckErrors.swift | 44 +++++++++++ .../Helpers/Help/DirectMessagesHelp.swift | 75 +++++++++++++++++++ .../Views/Helpers/Help/LockLegend.swift | 61 +++++++++++++++ Meshtastic/Views/Messages/UserList.swift | 35 ++++++--- 8 files changed, 258 insertions(+), 35 deletions(-) create mode 100644 Meshtastic/Views/Helpers/Help/AckErrors.swift create mode 100644 Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift create mode 100644 Meshtastic/Views/Helpers/Help/LockLegend.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 785091d8..798611fc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4810,9 +4810,6 @@ } } } - }, - "Contacts" : { - }, "contacts %@" : { "extractionState" : "migrated", @@ -6315,6 +6312,15 @@ }, "Direct" : { + }, + "Direct Message Help" : { + + }, + "Direct messages are using the new public key infrastructure to encrypt the message." : { + + }, + "Direct messages are using the shared key for the channel when communicating with this node." : { + }, "direct.messages" : { "localizations" : { @@ -7204,7 +7210,7 @@ "Favorites" : { }, - "Favorites and nodes with recent messages show up at the top of the list. Contacts using the shared key display an open lock, nodes with a private key show a green lock and a red key with a slash will show up if a key has changed for a contact. Long press to favorite or mute the contact or delete a conversation." : { + "Favorites and nodes with recent messages show up at the top of the contact list." : { }, "Fifteen Minutes" : { @@ -11272,6 +11278,9 @@ }, "Long Name: %@" : { + }, + "Long press to favorite or mute the contact or delete a conversation." : { + }, "Longitude" : { @@ -14527,6 +14536,9 @@ }, "Message" : { + }, + "Message Status Options" : { + }, "message.details" : { "localizations" : { @@ -15406,6 +15418,9 @@ } } } + }, + "Node Encryption Status" : { + }, "Node History" : { @@ -16665,6 +16680,9 @@ }, "Public Key" : { + }, + "Public Key Encryption" : { + }, "Public Key Mismatch" : { @@ -19763,6 +19781,9 @@ } } } + }, + "Shared Key" : { + }, "Short Name" : { @@ -20995,6 +21016,9 @@ }, "The public key authorized to send admin messages to this node." : { + }, + "The public key does not match the key that was used previously, delete the node and let it negotatiate keys again. Usually the other user did a factory reset, but it could indicate a security issue." : { + }, "The region where you will be using your radios." : { @@ -22519,6 +22543,9 @@ }, "Website" : { + }, + "What does the lock mean?" : { + }, "What is Meshtastic?" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2e083bd6..2c59e43b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -94,6 +94,9 @@ DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; + DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65752C6EA5490053C113 /* AckErrors.swift */; }; + DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */; }; + DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F657A2C6EC2900053C113 /* LockLegend.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; @@ -349,6 +352,9 @@ DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = ""; }; + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesHelp.swift; sourceTree = ""; }; + DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; @@ -710,6 +716,16 @@ path = Module; sourceTree = ""; }; + DD6F65772C6EAB860053C113 /* Help */ = { + isa = PBXGroup; + children = ( + DD6F65752C6EA5490053C113 /* AckErrors.swift */, + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */, + DD6F657A2C6EC2900053C113 /* LockLegend.swift */, + ); + path = Help; + sourceTree = ""; + }; DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( @@ -902,6 +918,7 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + DD6F65772C6EAB860053C113 /* Help */, DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, @@ -1300,6 +1317,7 @@ DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */, + DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */, 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */, DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, 25C49D902C471AEA0024FBD1 /* Constants.swift in Sources */, @@ -1358,6 +1376,7 @@ 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */, D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, + DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */, DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */, @@ -1385,6 +1404,7 @@ DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */, 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */, DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, + DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 4bdb8388..b47a15f9 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -54,7 +54,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .notAuthorized: return "routing.notauthorized".localized case .pkiFailed: - return "routing.pkiFailed".localized + return "routing.pkifailed".localized case .pkiUnknownPubkey: return "routing.pkiunknownpubkey".localized } @@ -77,7 +77,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .noChannel: return Color.orange case .tooLarge: - return Color.red + return Color.orange case .noResponse: return Color.orange case .dutyCycleLimit: diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index 5bd50d54..4771e386 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -25,22 +25,3 @@ struct MessagesTip: Tip { Image(systemName: "bubble.left.and.bubble.right") } } - -@available(iOS 17.0, macOS 14.0, *) -struct ContactsTip: Tip { - - var id: String { - return "tip.messages.contacts" - } - var title: Text { - // Text("tip.messages.contacts.title") - Text("Contacts") - } - var message: Text? { - // Text("tip.messages.contacts.message") - Text("Favorites and nodes with recent messages show up at the top of the list. Contacts using the shared key display an open lock, nodes with a private key show a green lock and a red key with a slash will show up if a key has changed for a contact. Long press to favorite or mute the contact or delete a conversation.") - } - var image: Image? { - Image(systemName: "person.circle") - } -} diff --git a/Meshtastic/Views/Helpers/Help/AckErrors.swift b/Meshtastic/Views/Helpers/Help/AckErrors.swift new file mode 100644 index 00000000..c20a58f9 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/AckErrors.swift @@ -0,0 +1,44 @@ +// +// IAQScale.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 4/24/24. +// + +import SwiftUI + +struct AckErrors: View { + + var body: some View { + VStack(alignment: .leading) { + Text("Message Status Options") + .font(.title2) + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(.orange) + .frame(width: 20, height: 12) + Text("Acknowledged by another node") + .font(.caption) + .foregroundStyle(.orange) + } + ForEach(RoutingError.allCases) { re in + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(re.color) + .frame(width: 20, height: 12) + Text(re.display) + .font(.caption) + .foregroundStyle(re.color) + } + } + } + } +} + +struct AckErrorsPreviews: PreviewProvider { + static var previews: some View { + VStack { + AckErrors() + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift new file mode 100644 index 00000000..dd57f29f --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift @@ -0,0 +1,75 @@ +// +// DirectMessagesHelp.swift +// Meshtastic +// +// Copyright Garth Vander Houwen on 8/15/24. +// + +import SwiftUI + +struct DirectMessagesHelp: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Environment(\.dismiss) private var dismiss + + var body: some View { + ScrollView { + Text("Direct Message Help") + .font(.title) + .padding(.vertical) + VStack(alignment: .leading) { + HStack { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .padding(.bottom) + Text("Favorites and nodes with recent messages show up at the top of the contact list.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "hand.tap") + .padding(.bottom) + Text("Long press to favorite or mute the contact or delete a conversation.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + } + if idiom == .phone { + VStack(alignment: .leading) { + LockLegend() + AckErrors() + } + } else { + HStack(alignment: .top) { + LockLegend() + AckErrors() + } + } +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .frame(minHeight: 0, maxHeight: .infinity, alignment: .leading) + .padding() + .presentationDetents([.large]) + .presentationContentInteraction(.scrolls) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) + } +} + +struct DirectMessagesHelpPreviews: PreviewProvider { + static var previews: some View { + VStack { + AckErrors() + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift new file mode 100644 index 00000000..b47aaf20 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -0,0 +1,61 @@ +// +// LockLegend.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 8/15/24. +// + +import SwiftUI + +struct LockLegend: View { + + var body: some View { + VStack(alignment: .leading) { + Text("Node Encryption Status") + .font(.title2) + Text("What does the lock mean?") + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + Text("Shared Key") + .fontWeight(.semibold) + } + Text("Direct messages are using the shared key for the channel when communicating with this node.") + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "lock.fill") + .foregroundColor(.green) + Text("Public Key Encryption") + .fontWeight(.semibold) + } + Text("Direct messages are using the new public key infrastructure to encrypt the message.") + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "key.slash") + .foregroundColor(.red) + Text("Public Key Mismatch") + .fontWeight(.semibold) + } + Text("The public key does not match the key that was used previously, delete the node and let it negotatiate keys again. Usually the other user did a factory reset, but it could indicate a security issue.") + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + } + } +} + +struct LockLegendPreviews: PreviewProvider { + static var previews: some View { + VStack { + LockLegend() + } + } +} diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 5294c752..e3d07d3a 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -28,9 +28,10 @@ struct UserList: View { @State private var hopsAway: Double = -1.0 @State private var roleFilter = false @State private var deviceRoles: Set = [] - @State var isEditingFilters = false + @State private var editingFilters = false + @State private var showingHelp = false @State private var showingTrustConfirm: Bool = false - + var boolFilters: [Bool] {[ isFavorite, isOnline, @@ -59,9 +60,6 @@ struct UserList: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { List(selection: $userSelection) { - if #available(iOS 17.0, macOS 14.0, *) { - TipView(ContactsTip(), arrowEdge: .bottom) - } ForEach(users) { (user: UserEntity) in let mostRecent = user.messageList.last let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) @@ -196,9 +194,12 @@ struct UserList: View { } .listStyle(.plain) .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count))) - .sheet(isPresented: $isEditingFilters) { + .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } + .sheet(isPresented: $showingHelp) { + DirectMessagesHelp() + } .onChange(of: searchText) { _ in searchUserList() } @@ -229,25 +230,39 @@ struct UserList: View { .onFirstAppear { searchUserList() } - .safeAreaInset(edge: .bottom, alignment: .trailing) { + .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { Button(action: { withAnimation { - isEditingFilters = !isEditingFilters + showingHelp = !showingHelp } }) { - Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + Image(systemName: !editingFilters ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + + Spacer() + + Button(action: { + withAnimation { + editingFilters = !editingFilters + } + }) { + Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - } .controlSize(.regular) .padding(5) } .padding(.bottom, 5) + .padding(.bottom, 5) .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) From 9345f4f1391c0596d17070c4edf675531bd36ca6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 16:39:04 -0700 Subject: [PATCH 113/333] Set version back --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2c59e43b..58f16fe5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1649,7 +1649,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1684,7 +1684,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1716,7 +1716,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1749,7 +1749,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.4.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From bbc6d4d40a9f5bdbf5cc67a47cfc50c512a151e2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 20:20:59 -0700 Subject: [PATCH 114/333] Updated help text. --- Localizable.xcstrings | 7 ++----- .../Views/Helpers/Help/DirectMessagesHelp.swift | 1 + Meshtastic/Views/Helpers/Help/LockLegend.swift | 15 ++++++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 798611fc..514c0443 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6319,7 +6319,7 @@ "Direct messages are using the new public key infrastructure to encrypt the message." : { }, - "Direct messages are using the shared key for the channel when communicating with this node." : { + "Direct messages are using the shared key for the channel." : { }, "direct.messages" : { @@ -15418,9 +15418,6 @@ } } } - }, - "Node Encryption Status" : { - }, "Node History" : { @@ -21017,7 +21014,7 @@ "The public key authorized to send admin messages to this node." : { }, - "The public key does not match the key that was used previously, delete the node and let it negotatiate keys again. Usually the other user did a factory reset, but it could indicate a security issue." : { + "The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action." : { }, "The region where you will be using your radios." : { diff --git a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift index dd57f29f..f4f8bda3 100644 --- a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift +++ b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift @@ -42,6 +42,7 @@ struct DirectMessagesHelp: View { HStack(alignment: .top) { LockLegend() AckErrors() + .padding(.trailing) } } #if targetEnvironment(macCatalyst) diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift index b47aaf20..adfa91b5 100644 --- a/Meshtastic/Views/Helpers/Help/LockLegend.swift +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -11,10 +11,9 @@ struct LockLegend: View { var body: some View { VStack(alignment: .leading) { - Text("Node Encryption Status") - .font(.title2) Text("What does the lock mean?") - .padding(.bottom) + .font(.title2) + .padding(.bottom, 5) VStack(alignment: .leading) { HStack { Image(systemName: "lock.open.fill") @@ -22,7 +21,9 @@ struct LockLegend: View { Text("Shared Key") .fontWeight(.semibold) } - Text("Direct messages are using the shared key for the channel when communicating with this node.") + Text("Direct messages are using the shared key for the channel.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) .fixedSize(horizontal: false, vertical: true) } .padding(.bottom) @@ -34,6 +35,8 @@ struct LockLegend: View { .fontWeight(.semibold) } Text("Direct messages are using the new public key infrastructure to encrypt the message.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) .fixedSize(horizontal: false, vertical: true) } .padding(.bottom) @@ -44,7 +47,9 @@ struct LockLegend: View { Text("Public Key Mismatch") .fontWeight(.semibold) } - Text("The public key does not match the key that was used previously, delete the node and let it negotatiate keys again. Usually the other user did a factory reset, but it could indicate a security issue.") + Text("The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) .fixedSize(horizontal: false, vertical: true) } .padding(.bottom) From 0ac32df941a15f67af8e8a48aaf87ac5a90d84a1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 16 Aug 2024 07:55:23 -0700 Subject: [PATCH 115/333] Bump versions --- Localizable.xcstrings | 5 +-- Meshtastic.xcodeproj/project.pbxproj | 8 ++-- Meshtastic/Enums/RoutingError.swift | 37 ++++++++++++++++++- .../Helpers/Help/DirectMessagesHelp.swift | 2 +- .../Views/Helpers/Help/LockLegend.swift | 2 +- .../Views/Nodes/Helpers/NodeDetail.swift | 2 +- Meshtastic/Views/Settings/Firmware.swift | 2 +- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 514c0443..671a3838 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6316,7 +6316,7 @@ "Direct Message Help" : { }, - "Direct messages are using the new public key infrastructure to encrypt the message." : { + "Direct messages are using the new public key infrastructure for encryption." : { }, "Direct messages are using the shared key for the channel." : { @@ -10790,9 +10790,6 @@ }, "Key" : { - }, - "Key does not match the public key that was used previously, delete the node and let it negotatiate keys again." : { - }, "Key Mapping" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 58f16fe5..2c59e43b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1649,7 +1649,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1684,7 +1684,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1716,7 +1716,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1749,7 +1749,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index b47a15f9..c7588b0c 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -73,11 +73,11 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .noInterface: return Color.red case .maxRetransmit: - return Color.red + return Color.orange case .noChannel: return Color.orange case .tooLarge: - return Color.orange + return Color.red case .noResponse: return Color.orange case .dutyCycleLimit: @@ -92,6 +92,39 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Color.red } } + var canRetry: Bool { + switch self { + + case .none: + return false + case .noRoute: + return false + case .gotNak: + return false + case .timeout: + return true + case .noInterface: + return false + case .maxRetransmit: + return true + case .noChannel: + return true + case .tooLarge: + return false + case .noResponse: + return true + case .dutyCycleLimit: + return true + case .badRequest: + return false + case .notAuthorized: + return false + case .pkiFailed: + return false + case .pkiUnknownPubkey: + return false + } + } func protoEnumValue() -> Routing.Error { switch self { diff --git a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift index f4f8bda3..fd0f0414 100644 --- a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift +++ b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift @@ -13,7 +13,7 @@ struct DirectMessagesHelp: View { var body: some View { ScrollView { - Text("Direct Message Help") + Label("Direct Message Help", systemImage: "questionmark.circle") .font(.title) .padding(.vertical) VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift index adfa91b5..9c5d216b 100644 --- a/Meshtastic/Views/Helpers/Help/LockLegend.swift +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -34,7 +34,7 @@ struct LockLegend: View { Text("Public Key Encryption") .fontWeight(.semibold) } - Text("Direct messages are using the new public key infrastructure to encrypt the message.") + Text("Direct messages are using the new public key infrastructure for encryption.") .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .font(.callout) .fixedSize(horizontal: false, vertical: true) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index e8eeeecd..8a78c08c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -51,7 +51,7 @@ struct NodeDetail: View { Text("Public Key Mismatch") .font(.title3) .foregroundStyle(.red) - Text("Key does not match the public key that was used previously, delete the node and let it negotatiate keys again.") + Text("The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.") .font(.caption) .foregroundStyle(.red) } diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index d76b1503..9a450775 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -13,7 +13,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.4.3" + @State var minimumVersion = "2.4.2" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? From e7f6402e5166164ada11431d88f064debf2397cb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 16 Aug 2024 09:38:09 -0700 Subject: [PATCH 116/333] Hook ack errors up to the canRetry bool --- Meshtastic/Extensions/CoreData/MessageEntityExtension.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 18d97f6c..70f36c3d 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -19,7 +19,8 @@ extension MessageEntity { } var canRetry: Bool { - return ackError == 9 || ackError == 5 || ackError == 3 + let re = RoutingError(rawValue: Int(ackError)) + return re?.canRetry ?? false } var tapbacks: [MessageEntity] { From 81084976db7d845192edca0660cb32bc5c3c8ae1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 16 Aug 2024 10:44:56 -0700 Subject: [PATCH 117/333] Hops away for position popower --- Localizable.xcstrings | 3 ++ .../Nodes/Helpers/Map/PositionPopover.swift | 34 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 671a3838..16d61fea 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -8281,6 +8281,9 @@ }, "Hops Away:" : { + }, + "Hops Away: %d" : { + }, "Hour" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index b3b9c18e..0c708058 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -14,6 +14,7 @@ struct PositionPopover: View { @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.dismiss) private var dismiss var position: PositionEntity var popover: Bool = true @@ -52,9 +53,13 @@ struct PositionPopover: View { VStack(alignment: .leading) { /// Time Label { - Text("heard".localized + ":") + if idiom != .phone { + Text("heard".localized + ":") + } LastHeardText(lastHeard: position.time) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) } icon: { Image(systemName: position.nodePosition?.isOnline ?? false ? "checkmark.circle.fill" : "moon.circle.fill") .symbolRenderingMode(.hierarchical) @@ -67,12 +72,28 @@ struct PositionPopover: View { Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))") .textSelection(.enabled) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) } icon: { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) + /// Hops Away + if position.nodePosition?.hopsAway ?? 0 > 0 { + Label { + Text("Hops Away: \(position.nodePosition?.hopsAway ?? 0)") + .textSelection(.enabled) + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "hare") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } /// Altitude Label { let formatter = MeasurementFormatter() @@ -81,9 +102,11 @@ struct PositionPopover: View { if Locale.current.measurementSystem == .metric { Text(altitudeFormatter.string(from: distanceInMeters)) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } else { Text(altitudeFormatter.string(from: distanceInFeet)) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } } icon: { @@ -98,6 +121,7 @@ struct PositionPopover: View { Label { Text("Sats in view: \(String(position.satsInView))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "sparkles") .symbolRenderingMode(.hierarchical) @@ -110,6 +134,7 @@ struct PositionPopover: View { Label { Text("Sequence: \(String(position.seqNo))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "number") .symbolRenderingMode(.hierarchical) @@ -134,6 +159,7 @@ struct PositionPopover: View { Label { Text("Speed: \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "gauge.with.dots.needle.33percent") .symbolRenderingMode(.hierarchical) @@ -144,6 +170,7 @@ struct PositionPopover: View { Label { Text("MQTT") + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "network") .symbolRenderingMode(.hierarchical) @@ -159,6 +186,7 @@ struct PositionPopover: View { Label { Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "lines.measurement.horizontal") .symbolRenderingMode(.hierarchical) @@ -223,9 +251,9 @@ struct PositionPopover: View { #endif } } - .presentationDetents([.medium, .large]) + .presentationDetents([.fraction(0.65), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) - .presentationBackgroundInteraction(.enabled(upThrough: .medium)) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } From 7d63a21418d6308200caeb798bf380bc6649a041 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 16 Aug 2024 16:26:54 -0700 Subject: [PATCH 118/333] Hook retry up to ack errors --- Meshtastic/Enums/RoutingError.swift | 45 +++++----------------- Meshtastic/Views/Messages/UserList.swift | 2 - Meshtastic/Views/Nodes/MeshMap.swift | 49 ++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 41 deletions(-) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index c7588b0c..3af6e9b2 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -60,55 +60,30 @@ enum RoutingError: Int, CaseIterable, Identifiable { } } var color: Color { - switch self { - - case .none: + if self == .none { return Color.secondary - case .noRoute: - return Color.red - case .gotNak: - return Color.red - case .timeout: + } else if self.canRetry { return Color.orange - case .noInterface: - return Color.red - case .maxRetransmit: - return Color.orange - case .noChannel: - return Color.orange - case .tooLarge: - return Color.red - case .noResponse: - return Color.orange - case .dutyCycleLimit: - return Color.orange - case .badRequest: - return Color.red - case .notAuthorized: - return Color.red - case .pkiFailed: - return Color.red - case .pkiUnknownPubkey: + } else { return Color.red } } var canRetry: Bool { switch self { - case .none: return false case .noRoute: - return false + return true case .gotNak: - return false + return true case .timeout: return true case .noInterface: - return false + return true case .maxRetransmit: - return true + return false case .noChannel: - return true + return false case .tooLarge: return false case .noResponse: @@ -116,9 +91,9 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .dutyCycleLimit: return true case .badRequest: - return false + return true case .notAuthorized: - return false + return true case .pkiFailed: return false case .pkiUnknownPubkey: diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index e3d07d3a..cb3582ab 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -243,9 +243,7 @@ struct UserList: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - Spacer() - Button(action: { withAnimation { editingFilters = !editingFilters diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 2dc8d105..31923562 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -33,13 +33,27 @@ struct MeshMap: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .excludingAll, showsTraffic: false) @State var position = MapCameraPosition.automatic - @State var isEditingSettings = false + @State private var editingSettings = false + @State private var editingFilters = false @State var selectedPosition: PositionEntity? @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? @State var selectedWaypointId: String? @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true + /// Filter + @State private var searchText = "" + @State private var viaLora = true + @State private var viaMqtt = true + @State private var isOnline = false + @State private var isPkiEncrypted = false + @State private var isFavorite = false + @State private var isEnvironment = false + @State private var distanceFilter = false + @State private var maxDistance: Double = 800000 + @State private var hopsAway: Double = -1.0 + @State private var roleFilter = false + @State private var deviceRoles: Set = [] var body: some View { @@ -106,7 +120,7 @@ struct MeshMap: View { WaypointForm(waypoint: selection, editMode: true) .padding() } - .sheet(isPresented: $isEditingSettings) { + .sheet(isPresented: $editingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) } .onChange(of: router.navigationState) { @@ -128,14 +142,41 @@ struct MeshMap: View { return } } + .sheet(isPresented: $editingFilters) { + NodeListFilter( + viaLora: $viaLora, + viaMqtt: $viaMqtt, + isOnline: $isOnline, + isPkiEncrypted: $isPkiEncrypted, + isFavorite: $isFavorite, + isEnvironment: $isEnvironment, + distanceFilter: $distanceFilter, + maximumDistance: $maxDistance, + hopsAway: $hopsAway, + roleFilter: $roleFilter, + deviceRoles: $deviceRoles + ) + } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { Button(action: { withAnimation { - isEditingSettings = !isEditingSettings + editingSettings = !editingSettings } }) { - Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle") + Image(systemName: editingSettings ? "info.circle.fill" : "info.circle") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + Spacer() + Button(action: { + withAnimation { + editingFilters = !editingFilters + } + }) { + Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) From 385663478249d32ec6bef2bdaef6cb803abf41e2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 16 Aug 2024 21:20:22 -0700 Subject: [PATCH 119/333] update protobufs --- .../Sources/meshtastic/admin.pb.swift | 311 +++++++++-- .../Sources/meshtastic/apponly.pb.swift | 6 +- .../Sources/meshtastic/atak.pb.swift | 64 ++- .../meshtastic/cannedmessages.pb.swift | 6 +- .../Sources/meshtastic/channel.pb.swift | 37 +- .../Sources/meshtastic/clientonly.pb.swift | 6 +- .../Sources/meshtastic/config.pb.swift | 445 +++++++++------- .../meshtastic/connection_status.pb.swift | 21 +- .../Sources/meshtastic/deviceonly.pb.swift | 38 +- .../Sources/meshtastic/localonly.pb.swift | 9 +- .../Sources/meshtastic/mesh.pb.swift | 501 ++++++++++++++---- .../Sources/meshtastic/module_config.pb.swift | 263 ++++++--- .../Sources/meshtastic/mqtt.pb.swift | 9 +- .../Sources/meshtastic/paxcount.pb.swift | 6 +- .../Sources/meshtastic/portnums.pb.swift | 14 +- .../Sources/meshtastic/powermon.pb.swift | 115 ++-- .../meshtastic/remote_hardware.pb.swift | 35 +- .../Sources/meshtastic/rtttl.pb.swift | 6 +- .../Sources/meshtastic/storeforward.pb.swift | 93 +++- .../Sources/meshtastic/telemetry.pb.swift | 217 +++++++- .../Sources/meshtastic/xmodem.pb.swift | 39 +- protobufs | 2 +- 22 files changed, 1666 insertions(+), 577 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 4028fdcd..0f1c3586 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -24,11 +24,17 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage: Sendable { +public struct AdminMessage { // 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. + /// + /// The node generates this key and sends it with any get_x_response packets. + /// The client MUST include the same key with any set_x commands. Key expires after 300 seconds. + /// Prevents replay attacks for admin messages. + public var sessionPasskey: Data = Data() + /// /// TODO: REPLACE public var payloadVariant: AdminMessage.OneOf_PayloadVariant? = nil @@ -369,6 +375,17 @@ public struct AdminMessage: Sendable { set {payloadVariant = .removeFixedPosition(newValue)} } + /// + /// Set time only on the node + /// Convenience method to set the time on the node (as Net quality) without any other position data + public var setTimeOnly: UInt32 { + get { + if case .setTimeOnly(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .setTimeOnly(newValue)} + } + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) @@ -466,7 +483,7 @@ public struct AdminMessage: Sendable { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -573,6 +590,10 @@ public struct AdminMessage: Sendable { /// Clear fixed position coordinates and then set position.fixed_position = false case removeFixedPosition(Bool) /// + /// Set time only on the node + /// Convenience method to set the time on the node (as Net quality) without any other position data + case setTimeOnly(UInt32) + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) case beginEditSettings(Bool) @@ -603,11 +624,193 @@ public struct AdminMessage: Sendable { /// Tell the node to reset the nodedb. case nodedbReset(Int32) + #if !swift(>=4.1) + public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.getChannelRequest, .getChannelRequest): return { + guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getChannelResponse, .getChannelResponse): return { + guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getOwnerRequest, .getOwnerRequest): return { + guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getOwnerResponse, .getOwnerResponse): return { + guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getConfigRequest, .getConfigRequest): return { + guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getConfigResponse, .getConfigResponse): return { + guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getModuleConfigRequest, .getModuleConfigRequest): return { + guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getModuleConfigResponse, .getModuleConfigResponse): return { + guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { + guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { + guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { + guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { + guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getRingtoneRequest, .getRingtoneRequest): return { + guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getRingtoneResponse, .getRingtoneResponse): return { + guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { + guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { + guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setHamMode, .setHamMode): return { + guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { + guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { + guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.enterDfuModeRequest, .enterDfuModeRequest): return { + guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.deleteFileRequest, .deleteFileRequest): return { + guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setScale, .setScale): return { + guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setOwner, .setOwner): return { + guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setChannel, .setChannel): return { + guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setConfig, .setConfig): return { + guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setModuleConfig, .setModuleConfig): return { + guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { + guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setRingtoneMessage, .setRingtoneMessage): return { + guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeByNodenum, .removeByNodenum): return { + guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFavoriteNode, .setFavoriteNode): return { + guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFavoriteNode, .removeFavoriteNode): return { + guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFixedPosition, .setFixedPosition): return { + guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFixedPosition, .removeFixedPosition): return { + guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setTimeOnly, .setTimeOnly): return { + guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.beginEditSettings, .beginEditSettings): return { + guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.commitEditSettings, .commitEditSettings): return { + guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.factoryResetDevice, .factoryResetDevice): return { + guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebootOtaSeconds, .rebootOtaSeconds): return { + guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.exitSimulator, .exitSimulator): return { + guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebootSeconds, .rebootSeconds): return { + guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.shutdownSeconds, .shutdownSeconds): return { + guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.factoryResetConfig, .factoryResetConfig): return { + guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.nodedbReset, .nodedbReset): return { + guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ConfigType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -675,23 +878,11 @@ public struct AdminMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - .securityConfig, - ] - } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ModuleConfigType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -789,31 +980,51 @@ public struct AdminMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] - } public init() {} } +#if swift(>=4.2) + +extension AdminMessage.ConfigType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + .securityConfig, + ] +} + +extension AdminMessage.ModuleConfigType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] +} + +#endif // swift(>=4.2) + /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters: Sendable { +public struct HamParameters { // 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. @@ -843,7 +1054,7 @@ public struct HamParameters: Sendable { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse: Sendable { +public struct NodeRemoteHardwarePinsResponse { // 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. @@ -857,6 +1068,15 @@ public struct NodeRemoteHardwarePinsResponse: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension AdminMessage: @unchecked Sendable {} +extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} +extension AdminMessage.ConfigType: @unchecked Sendable {} +extension AdminMessage.ModuleConfigType: @unchecked Sendable {} +extension HamParameters: @unchecked Sendable {} +extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -864,6 +1084,7 @@ fileprivate let _protobuf_package = "meshtastic" extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AdminMessage" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 101: .standard(proto: "session_passkey"), 1: .standard(proto: "get_channel_request"), 2: .standard(proto: "get_channel_response"), 3: .standard(proto: "get_owner_request"), @@ -897,6 +1118,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 40: .standard(proto: "remove_favorite_node"), 41: .standard(proto: "set_fixed_position"), 42: .standard(proto: "remove_fixed_position"), + 43: .standard(proto: "set_time_only"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), 94: .standard(proto: "factory_reset_device"), @@ -1243,6 +1465,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .removeFixedPosition(v) } }() + case 43: try { + var v: UInt32? + try decoder.decodeSingularFixed32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .setTimeOnly(v) + } + }() case 64: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1315,6 +1545,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .nodedbReset(v) } }() + case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }() default: break } } @@ -1458,6 +1689,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .removeFixedPosition(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 42) }() + case .setTimeOnly?: try { + guard case .setTimeOnly(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularFixed32Field(value: v, fieldNumber: 43) + }() case .beginEditSettings?: try { guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 64) @@ -1496,10 +1731,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat }() case nil: break } + if !self.sessionPasskey.isEmpty { + try visitor.visitSingularBytesField(value: self.sessionPasskey, fieldNumber: 101) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: AdminMessage, rhs: AdminMessage) -> Bool { + if lhs.sessionPasskey != rhs.sessionPasskey {return false} if lhs.payloadVariant != rhs.payloadVariant {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -1568,7 +1807,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency.bitPattern != 0 { + if self.frequency != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 18e66d8e..0457077c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet: Sendable { +public struct ChannelSet { // 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. @@ -53,6 +53,10 @@ public struct ChannelSet: Sendable { fileprivate var _loraConfig: Config.LoRaConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension ChannelSet: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 1dd12469..4406deb3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum Team: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -130,6 +130,11 @@ public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -148,12 +153,13 @@ public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { .darkGreen, .brown, ] - } +#endif // swift(>=4.2) + /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum MemberRole: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -227,6 +233,11 @@ public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -239,12 +250,13 @@ public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { .rto, .k9, ] - } +#endif // swift(>=4.2) + /// /// Packets for the official ATAK Plugin -public struct TAKPacket: Sendable { +public struct TAKPacket { // 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. @@ -314,7 +326,7 @@ public struct TAKPacket: Sendable { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TAK position report case pli(PLI) @@ -322,6 +334,24 @@ public struct TAKPacket: Sendable { /// ATAK GeoChat message case chat(GeoChat) + #if !swift(>=4.1) + public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.pli, .pli): return { + guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.chat, .chat): return { + guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -333,7 +363,7 @@ public struct TAKPacket: Sendable { /// /// ATAK GeoChat message -public struct GeoChat: Sendable { +public struct GeoChat { // 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. @@ -375,7 +405,7 @@ public struct GeoChat: Sendable { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group: Sendable { +public struct Group { // 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. @@ -397,7 +427,7 @@ public struct Group: Sendable { /// /// ATAK EUD Status /// -public struct Status: Sendable { +public struct Status { // 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. @@ -414,7 +444,7 @@ public struct Status: Sendable { /// /// ATAK Contact /// -public struct Contact: Sendable { +public struct Contact { // 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. @@ -434,7 +464,7 @@ public struct Contact: Sendable { /// /// Position Location Information from ATAK -public struct PLI: Sendable { +public struct PLI { // 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. @@ -466,6 +496,18 @@ public struct PLI: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Team: @unchecked Sendable {} +extension MemberRole: @unchecked Sendable {} +extension TAKPacket: @unchecked Sendable {} +extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} +extension GeoChat: @unchecked Sendable {} +extension Group: @unchecked Sendable {} +extension Status: @unchecked Sendable {} +extension Contact: @unchecked Sendable {} +extension PLI: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index a43393e1..1b8c84de 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig: Sendable { +public struct CannedMessageModuleConfig { // 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. @@ -36,6 +36,10 @@ public struct CannedMessageModuleConfig: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension CannedMessageModuleConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index a8c96595..5b9c7e49 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -36,15 +36,13 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings: @unchecked Sendable { +public struct ChannelSettings { // 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. /// /// Deprecated in favor of LoraConfig.channel_num - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -113,7 +111,7 @@ public struct ChannelSettings: @unchecked Sendable { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings: Sendable { +public struct ModuleSettings { // 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. @@ -134,7 +132,7 @@ public struct ModuleSettings: Sendable { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel: Sendable { +public struct Channel { // 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. @@ -172,7 +170,7 @@ public struct Channel: Sendable { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Role: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -211,13 +209,6 @@ public struct Channel: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] - } public init() {} @@ -225,6 +216,26 @@ public struct Channel: Sendable { fileprivate var _settings: ChannelSettings? = nil } +#if swift(>=4.2) + +extension Channel.Role: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension ChannelSettings: @unchecked Sendable {} +extension ModuleSettings: @unchecked Sendable {} +extension Channel: @unchecked Sendable {} +extension Channel.Role: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index 89370cc5..c3d93bf7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile: Sendable { +public struct DeviceProfile { // 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. @@ -94,6 +94,10 @@ public struct DeviceProfile: Sendable { fileprivate var _moduleConfig: LocalModuleConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension DeviceProfile: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index ecfeeefa..4b953470 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config: Sendable { +public struct Config { // 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. @@ -97,7 +97,7 @@ public struct Config: Sendable { /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -107,11 +107,53 @@ public struct Config: Sendable { case bluetooth(Config.BluetoothConfig) case security(Config.SecurityConfig) + #if !swift(>=4.1) + public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.device, .device): return { + guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.position, .position): return { + guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.power, .power): return { + guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.network, .network): return { + guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.display, .display): return { + guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.lora, .lora): return { + guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.bluetooth, .bluetooth): return { + guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.security, .security): return { + guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// Configuration - public struct DeviceConfig: Sendable { + public struct DeviceConfig { // 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. @@ -123,16 +165,12 @@ public struct Config: Sendable { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI /// Moved to SecurityConfig - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var serialEnabled: Bool = false /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). /// Set this to true to leave the debug log outputting even when API is active. /// Moved to SecurityConfig - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var debugLogEnabled: Bool = false /// @@ -162,8 +200,6 @@ public struct Config: Sendable { /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface /// Moved to SecurityConfig - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var isManaged: Bool = false /// @@ -182,7 +218,7 @@ public struct Config: Sendable { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Role: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -200,8 +236,6 @@ public struct Config: Sendable { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 - - /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -292,26 +326,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - ] - } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RebroadcastMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -359,14 +378,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - ] - } public init() {} @@ -374,7 +385,7 @@ public struct Config: Sendable { /// /// Position Config - public struct PositionConfig: Sendable { + public struct PositionConfig { // 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. @@ -396,8 +407,6 @@ public struct Config: Sendable { /// /// Is GPS enabled for this node? - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -408,8 +417,6 @@ public struct Config: Sendable { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -450,7 +457,7 @@ public struct Config: Sendable { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum PositionFlags: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -540,24 +547,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] - } - public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum GpsMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -595,13 +587,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] - } public init() {} @@ -610,7 +595,7 @@ public struct Config: Sendable { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig: Sendable { + public struct PowerConfig { // 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. @@ -670,7 +655,7 @@ public struct Config: Sendable { /// /// Network Config - public struct NetworkConfig: Sendable { + public struct NetworkConfig { // 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. @@ -717,7 +702,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum AddressMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -749,15 +734,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] - } - public struct IpV4Config: Sendable { + public struct IpV4Config { // 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. @@ -790,7 +769,7 @@ public struct Config: Sendable { /// /// Display Config - public struct DisplayConfig: Sendable { + public struct DisplayConfig { // 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. @@ -846,7 +825,7 @@ public struct Config: Sendable { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -909,21 +888,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] - } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum DisplayUnits: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -955,17 +924,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] - } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum OledType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1009,17 +972,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - ] - } - public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum DisplayMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1063,17 +1018,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] - } - public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum CompassOrientation: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1141,18 +1088,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] - } public init() {} @@ -1160,7 +1095,7 @@ public struct Config: Sendable { /// /// Lora Config - public struct LoRaConfig: @unchecked Sendable { + public struct LoRaConfig { // 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. @@ -1317,7 +1252,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RegionCode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1451,35 +1386,12 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - ] - } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ModemPreset: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1493,8 +1405,6 @@ public struct Config: Sendable { /// /// Very Long Range - Slow /// Deprecated in 2.5: Works only with txco and is unusably slow - /// - /// NOTE: This enum value was marked as deprecated in the .proto file case veryLongSlow // = 2 /// @@ -1558,19 +1468,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - .shortTurbo, - ] - } public init() {} @@ -1578,7 +1475,7 @@ public struct Config: Sendable { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig: Sendable { + public struct BluetoothConfig { // 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. @@ -1598,13 +1495,11 @@ public struct Config: Sendable { /// /// Enables device (serial style logs) over Bluetooth /// Moved to SecurityConfig - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var deviceLoggingEnabled: Bool = false public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum PairingMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1642,19 +1537,12 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] - } public init() {} } - public struct SecurityConfig: @unchecked Sendable { + public struct SecurityConfig { // 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. @@ -1703,6 +1591,201 @@ public struct Config: Sendable { public init() {} } +#if swift(>=4.2) + +extension Config.DeviceConfig.Role: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + ] +} + +extension Config.DeviceConfig.RebroadcastMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + ] +} + +extension Config.PositionConfig.PositionFlags: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] +} + +extension Config.PositionConfig.GpsMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] +} + +extension Config.NetworkConfig.AddressMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] +} + +extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] +} + +extension Config.DisplayConfig.DisplayUnits: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] +} + +extension Config.DisplayConfig.OledType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + ] +} + +extension Config.DisplayConfig.DisplayMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] +} + +extension Config.DisplayConfig.CompassOrientation: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] +} + +extension Config.LoRaConfig.RegionCode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + ] +} + +extension Config.LoRaConfig.ModemPreset: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + .shortTurbo, + ] +} + +extension Config.BluetoothConfig.PairingMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension Config: @unchecked Sendable {} +extension Config.OneOf_PayloadVariant: @unchecked Sendable {} +extension Config.DeviceConfig: @unchecked Sendable {} +extension Config.DeviceConfig.Role: @unchecked Sendable {} +extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} +extension Config.PositionConfig: @unchecked Sendable {} +extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} +extension Config.PositionConfig.GpsMode: @unchecked Sendable {} +extension Config.PowerConfig: @unchecked Sendable {} +extension Config.NetworkConfig: @unchecked Sendable {} +extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} +extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} +extension Config.DisplayConfig: @unchecked Sendable {} +extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} +extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} +extension Config.DisplayConfig.OledType: @unchecked Sendable {} +extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} +extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} +extension Config.LoRaConfig: @unchecked Sendable {} +extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} +extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} +extension Config.BluetoothConfig: @unchecked Sendable {} +extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} +extension Config.SecurityConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2177,7 +2260,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride.bitPattern != 0 { + if self.adcMultiplierOverride != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2621,7 +2704,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset.bitPattern != 0 { + if _storage._frequencyOffset != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2645,7 +2728,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency.bitPattern != 0 { + if _storage._overrideFrequency != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a4569714..a2ec180e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus: Sendable { +public struct DeviceConnectionStatus { // 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus: Sendable { /// /// WiFi connection status -public struct WifiConnectionStatus: Sendable { +public struct WifiConnectionStatus { // 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus: Sendable { /// /// Ethernet connection status -public struct EthernetConnectionStatus: Sendable { +public struct EthernetConnectionStatus { // 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus: Sendable { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus: Sendable { +public struct NetworkConnectionStatus { // 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus: Sendable { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus: Sendable { +public struct BluetoothConnectionStatus { // 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus: Sendable { /// /// Serial connection status -public struct SerialConnectionStatus: Sendable { +public struct SerialConnectionStatus { // 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. @@ -209,6 +209,15 @@ public struct SerialConnectionStatus: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension DeviceConnectionStatus: @unchecked Sendable {} +extension WifiConnectionStatus: @unchecked Sendable {} +extension EthernetConnectionStatus: @unchecked Sendable {} +extension NetworkConnectionStatus: @unchecked Sendable {} +extension BluetoothConnectionStatus: @unchecked Sendable {} +extension SerialConnectionStatus: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 076e639e..43506399 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum ScreenFonts: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -60,18 +60,24 @@ public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, ] - } +#endif // swift(>=4.2) + /// /// Position with static location information only for NodeDBLite -public struct PositionLite: Sendable { +public struct PositionLite { // 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. @@ -106,15 +112,13 @@ public struct PositionLite: Sendable { public init() {} } -public struct UserLite: @unchecked Sendable { +public struct UserLite { // 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. /// /// This is the addr of the radio. - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -153,7 +157,7 @@ public struct UserLite: @unchecked Sendable { public init() {} } -public struct NodeInfoLite: @unchecked Sendable { +public struct NodeInfoLite { // 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. @@ -256,7 +260,7 @@ public struct NodeInfoLite: @unchecked Sendable { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState: @unchecked Sendable { +public struct DeviceState { // 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. @@ -316,8 +320,6 @@ public struct DeviceState: @unchecked Sendable { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -366,7 +368,7 @@ public struct DeviceState: @unchecked Sendable { /// /// The on-disk saved channels -public struct ChannelFile: Sendable { +public struct ChannelFile { // 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. @@ -389,7 +391,7 @@ public struct ChannelFile: Sendable { /// /// This can be used for customizing the firmware distribution. If populated, /// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore: @unchecked Sendable { +public struct OEMStore { // 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. @@ -448,6 +450,16 @@ public struct OEMStore: @unchecked Sendable { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension ScreenFonts: @unchecked Sendable {} +extension PositionLite: @unchecked Sendable {} +extension UserLite: @unchecked Sendable {} +extension NodeInfoLite: @unchecked Sendable {} +extension DeviceState: @unchecked Sendable {} +extension ChannelFile: @unchecked Sendable {} +extension OEMStore: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -683,7 +695,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr.bitPattern != 0 { + if _storage._snr != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index f2ef681d..0af27466 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig: @unchecked Sendable { +public struct LocalConfig { // 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. @@ -129,7 +129,7 @@ public struct LocalConfig: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig: @unchecked Sendable { +public struct LocalModuleConfig { // 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. @@ -293,6 +293,11 @@ public struct LocalModuleConfig: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=5.5) && canImport(_Concurrency) +extension LocalConfig: @unchecked Sendable {} +extension LocalModuleConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 2aa80dc1..1f4c60a5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -25,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum HardwareModel: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -515,6 +515,11 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -593,12 +598,13 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .radiomaster900Bandit, .privateHw, ] - } +#endif // swift(>=4.2) + /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum Constants: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -633,20 +639,26 @@ public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] - } +#endif // swift(>=4.2) + /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum CriticalErrorCode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -755,6 +767,11 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -772,12 +789,13 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { .flashCorruptionRecoverable, .flashCorruptionUnrecoverable, ] - } +#endif // swift(>=4.2) + /// /// a gps position -public struct Position: @unchecked Sendable { +public struct Position { // 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. @@ -994,7 +1012,7 @@ public struct Position: @unchecked Sendable { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum LocSource: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1038,20 +1056,12 @@ public struct Position: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] - } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum AltSource: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1101,15 +1111,6 @@ public struct Position: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] - } public init() {} @@ -1117,6 +1118,31 @@ public struct Position: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=4.2) + +extension Position.LocSource: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] +} + +extension Position.AltSource: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] +} + +#endif // swift(>=4.2) + /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1138,7 +1164,7 @@ public struct Position: @unchecked Sendable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User: @unchecked Sendable { +public struct User { // 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. @@ -1163,8 +1189,6 @@ public struct User: @unchecked Sendable { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1196,7 +1220,7 @@ public struct User: @unchecked Sendable { /// /// A message used in our Dynamic Source Routing protocol (RFC 4728 based) -public struct RouteDiscovery: Sendable { +public struct RouteDiscovery { // 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. @@ -1212,7 +1236,7 @@ public struct RouteDiscovery: Sendable { /// /// A Routing control Data packet handled by the routing module -public struct Routing: Sendable { +public struct Routing { // 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. @@ -1252,7 +1276,7 @@ public struct Routing: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable, Sendable { + public enum OneOf_Variant: Equatable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1264,12 +1288,34 @@ public struct Routing: Sendable { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) + #if !swift(>=4.1) + public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.routeRequest, .routeRequest): return { + guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.routeReply, .routeReply): return { + guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorReason, .errorReason): return { + guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Error: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1375,34 +1421,40 @@ public struct Routing: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - .pkiFailed, - .pkiUnknownPubkey, - ] - } public init() {} } +#if swift(>=4.2) + +extension Routing.Error: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + .pkiFailed, + .pkiUnknownPubkey, + ] +} + +#endif // swift(>=4.2) + /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage: @unchecked Sendable { +public struct DataMessage { // 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. @@ -1456,7 +1508,7 @@ public struct DataMessage: @unchecked Sendable { /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint: Sendable { +public struct Waypoint { // 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. @@ -1518,7 +1570,7 @@ public struct Waypoint: Sendable { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage: @unchecked Sendable { +public struct MqttClientProxyMessage { // 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. @@ -1559,7 +1611,7 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Bytes case data(Data) @@ -1567,6 +1619,24 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// Text case text(String) + #if !swift(>=4.1) + public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.data, .data): return { + guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -1576,7 +1646,7 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket: @unchecked Sendable { +public struct MeshPacket { // 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. @@ -1710,8 +1780,6 @@ public struct MeshPacket: @unchecked Sendable { /// /// Describe if this message is delayed - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -1748,7 +1816,7 @@ public struct MeshPacket: @unchecked Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1756,6 +1824,24 @@ public struct MeshPacket: @unchecked Sendable { /// TODO: REPLACE case encrypted(Data) + #if !swift(>=4.1) + public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.decoded, .decoded): return { + guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.encrypted, .encrypted): return { + guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// @@ -1777,7 +1863,7 @@ public struct MeshPacket: @unchecked Sendable { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Priority: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1842,22 +1928,11 @@ public struct MeshPacket: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .ack, - .max, - ] - } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Delayed: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1895,13 +1970,6 @@ public struct MeshPacket: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] - } public init() {} @@ -1909,6 +1977,32 @@ public struct MeshPacket: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=4.2) + +extension MeshPacket.Priority: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .ack, + .max, + ] +} + +extension MeshPacket.Delayed: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] +} + +#endif // swift(>=4.2) + /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -1926,7 +2020,7 @@ public struct MeshPacket: @unchecked Sendable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo: @unchecked Sendable { +public struct NodeInfo { // 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. @@ -2027,7 +2121,7 @@ public struct NodeInfo: @unchecked Sendable { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo: Sendable { +public struct MyNodeInfo { // 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. @@ -2058,7 +2152,7 @@ public struct MyNodeInfo: Sendable { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord: Sendable { +public struct LogRecord { // 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. @@ -2083,7 +2177,7 @@ public struct LogRecord: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Level: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -2145,23 +2239,29 @@ public struct LogRecord: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] - } public init() {} } -public struct QueueStatus: Sendable { +#if swift(>=4.2) + +extension LogRecord.Level: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] +} + +#endif // swift(>=4.2) + +public struct QueueStatus { // 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. @@ -2188,7 +2288,7 @@ public struct QueueStatus: Sendable { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio: Sendable { +public struct FromRadio { // 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. @@ -2364,7 +2464,7 @@ public struct FromRadio: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2419,6 +2519,76 @@ public struct FromRadio: Sendable { /// Notification message to the client case clientNotification(ClientNotification) + #if !swift(>=4.1) + public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.packet, .packet): return { + guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.myInfo, .myInfo): return { + guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.nodeInfo, .nodeInfo): return { + guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.config, .config): return { + guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.logRecord, .logRecord): return { + guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.configCompleteID, .configCompleteID): return { + guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebooted, .rebooted): return { + guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.moduleConfig, .moduleConfig): return { + guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.channel, .channel): return { + guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.queueStatus, .queueStatus): return { + guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.metadata, .metadata): return { + guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { + guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileInfo, .fileInfo): return { + guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.clientNotification, .clientNotification): return { + guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -2429,7 +2599,7 @@ public struct FromRadio: Sendable { /// To be used for important messages that should to be displayed to the user /// in the form of push notifications or validation messages when saving /// invalid configuration. -public struct ClientNotification: Sendable { +public struct ClientNotification { // 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. @@ -2466,7 +2636,7 @@ public struct ClientNotification: Sendable { /// /// Individual File info for the device -public struct FileInfo: Sendable { +public struct FileInfo { // 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. @@ -2487,7 +2657,7 @@ public struct FileInfo: Sendable { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio: Sendable { +public struct ToRadio { // 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. @@ -2567,7 +2737,7 @@ public struct ToRadio: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -2594,6 +2764,40 @@ public struct ToRadio: Sendable { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) + #if !swift(>=4.1) + public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.packet, .packet): return { + guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.wantConfigID, .wantConfigID): return { + guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.disconnect, .disconnect): return { + guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { + guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -2601,7 +2805,7 @@ public struct ToRadio: Sendable { /// /// Compressed message payload -public struct Compressed: @unchecked Sendable { +public struct Compressed { // 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. @@ -2621,7 +2825,7 @@ public struct Compressed: @unchecked Sendable { /// /// Full info on edges for a single node -public struct NeighborInfo: Sendable { +public struct NeighborInfo { // 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. @@ -2649,7 +2853,7 @@ public struct NeighborInfo: Sendable { /// /// A single edge in the mesh -public struct Neighbor: Sendable { +public struct Neighbor { // 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. @@ -2679,7 +2883,7 @@ public struct Neighbor: Sendable { /// /// Device metadata response -public struct DeviceMetadata: Sendable { +public struct DeviceMetadata { // 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. @@ -2732,7 +2936,7 @@ public struct DeviceMetadata: Sendable { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat: Sendable { +public struct Heartbeat { // 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. @@ -2744,7 +2948,7 @@ public struct Heartbeat: Sendable { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin: Sendable { +public struct NodeRemoteHardwarePin { // 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. @@ -2771,7 +2975,7 @@ public struct NodeRemoteHardwarePin: Sendable { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload: @unchecked Sendable { +public struct ChunkedPayload { // 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. @@ -2799,7 +3003,7 @@ public struct ChunkedPayload: @unchecked Sendable { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks: Sendable { +public struct resend_chunks { // 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. @@ -2813,7 +3017,7 @@ public struct resend_chunks: Sendable { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse: Sendable { +public struct ChunkedPayloadResponse { // 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. @@ -2856,7 +3060,7 @@ public struct ChunkedPayloadResponse: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -2867,11 +3071,76 @@ public struct ChunkedPayloadResponse: Sendable { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) + #if !swift(>=4.1) + public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.requestTransfer, .requestTransfer): return { + guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.acceptTransfer, .acceptTransfer): return { + guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.resendChunks, .resendChunks): return { + guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension HardwareModel: @unchecked Sendable {} +extension Constants: @unchecked Sendable {} +extension CriticalErrorCode: @unchecked Sendable {} +extension Position: @unchecked Sendable {} +extension Position.LocSource: @unchecked Sendable {} +extension Position.AltSource: @unchecked Sendable {} +extension User: @unchecked Sendable {} +extension RouteDiscovery: @unchecked Sendable {} +extension Routing: @unchecked Sendable {} +extension Routing.OneOf_Variant: @unchecked Sendable {} +extension Routing.Error: @unchecked Sendable {} +extension DataMessage: @unchecked Sendable {} +extension Waypoint: @unchecked Sendable {} +extension MqttClientProxyMessage: @unchecked Sendable {} +extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} +extension MeshPacket: @unchecked Sendable {} +extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} +extension MeshPacket.Priority: @unchecked Sendable {} +extension MeshPacket.Delayed: @unchecked Sendable {} +extension NodeInfo: @unchecked Sendable {} +extension MyNodeInfo: @unchecked Sendable {} +extension LogRecord: @unchecked Sendable {} +extension LogRecord.Level: @unchecked Sendable {} +extension QueueStatus: @unchecked Sendable {} +extension FromRadio: @unchecked Sendable {} +extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension ClientNotification: @unchecked Sendable {} +extension FileInfo: @unchecked Sendable {} +extension ToRadio: @unchecked Sendable {} +extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension Compressed: @unchecked Sendable {} +extension NeighborInfo: @unchecked Sendable {} +extension Neighbor: @unchecked Sendable {} +extension DeviceMetadata: @unchecked Sendable {} +extension Heartbeat: @unchecked Sendable {} +extension NodeRemoteHardwarePin: @unchecked Sendable {} +extension ChunkedPayload: @unchecked Sendable {} +extension resend_chunks: @unchecked Sendable {} +extension ChunkedPayloadResponse: @unchecked Sendable {} +extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -3846,7 +4115,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr.bitPattern != 0 { + if _storage._rxSnr != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -4029,7 +4298,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr.bitPattern != 0 { + if _storage._snr != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -4874,7 +5143,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr.bitPattern != 0 { + if self.snr != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -4987,8 +5256,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 6f3b2d76..c68ffd83 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -58,18 +58,24 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] - } +#endif // swift(>=4.2) + /// /// Module Config -public struct ModuleConfig: Sendable { +public struct ModuleConfig { // 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. @@ -212,7 +218,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -253,11 +259,73 @@ public struct ModuleConfig: Sendable { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) + #if !swift(>=4.1) + public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.mqtt, .mqtt): return { + guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.serial, .serial): return { + guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.externalNotification, .externalNotification): return { + guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.storeForward, .storeForward): return { + guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rangeTest, .rangeTest): return { + guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.telemetry, .telemetry): return { + guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.cannedMessage, .cannedMessage): return { + guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.audio, .audio): return { + guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.remoteHardware, .remoteHardware): return { + guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.neighborInfo, .neighborInfo): return { + guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.ambientLighting, .ambientLighting): return { + guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.detectionSensor, .detectionSensor): return { + guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.paxcounter, .paxcounter): return { + guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// MQTT Client Config - public struct MQTTConfig: Sendable { + public struct MQTTConfig { // 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. @@ -332,7 +400,7 @@ public struct ModuleConfig: Sendable { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings: Sendable { + public struct MapReportSettings { // 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. @@ -352,7 +420,7 @@ public struct ModuleConfig: Sendable { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig: Sendable { + public struct RemoteHardwareConfig { // 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. @@ -376,7 +444,7 @@ public struct ModuleConfig: Sendable { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig: Sendable { + public struct NeighborInfoConfig { // 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. @@ -397,7 +465,7 @@ public struct ModuleConfig: Sendable { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig: Sendable { + public struct DetectionSensorConfig { // 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. @@ -448,7 +516,7 @@ public struct ModuleConfig: Sendable { /// /// Audio Config for codec2 voice - public struct AudioConfig: Sendable { + public struct AudioConfig { // 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. @@ -485,7 +553,7 @@ public struct ModuleConfig: Sendable { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Audio_Baud: SwiftProtobuf.Enum { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -532,19 +600,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] - } public init() {} @@ -552,7 +607,7 @@ public struct ModuleConfig: Sendable { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig: Sendable { + public struct PaxcounterConfig { // 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. @@ -578,7 +633,7 @@ public struct ModuleConfig: Sendable { /// /// Serial Config - public struct SerialConfig: Sendable { + public struct SerialConfig { // 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. @@ -621,7 +676,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Serial_Baud: SwiftProtobuf.Enum { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -689,31 +744,11 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] - } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Serial_Mode: SwiftProtobuf.Enum { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -758,17 +793,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] - } public init() {} @@ -776,7 +800,7 @@ public struct ModuleConfig: Sendable { /// /// External Notifications Config - public struct ExternalNotificationConfig: Sendable { + public struct ExternalNotificationConfig { // 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. @@ -859,7 +883,7 @@ public struct ModuleConfig: Sendable { /// /// Store and Forward Module Config - public struct StoreForwardConfig: Sendable { + public struct StoreForwardConfig { // 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. @@ -895,7 +919,7 @@ public struct ModuleConfig: Sendable { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig: Sendable { + public struct RangeTestConfig { // 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. @@ -920,7 +944,7 @@ public struct ModuleConfig: Sendable { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig: Sendable { + public struct TelemetryConfig { // 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. @@ -977,7 +1001,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public struct CannedMessageConfig: Sendable { + public struct CannedMessageConfig { // 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. @@ -1032,7 +1056,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum InputEventChar: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1100,18 +1124,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] - } public init() {} @@ -1120,7 +1132,7 @@ public struct ModuleConfig: Sendable { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig: Sendable { + public struct AmbientLightingConfig { // 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. @@ -1153,9 +1165,77 @@ public struct ModuleConfig: Sendable { public init() {} } +#if swift(>=4.2) + +extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] +} + +extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] +} + +extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + ] +} + +extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] +} + +#endif // swift(>=4.2) + /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin: Sendable { +public struct RemoteHardwarePin { // 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. @@ -1177,6 +1257,31 @@ public struct RemoteHardwarePin: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension RemoteHardwarePinType: @unchecked Sendable {} +extension ModuleConfig: @unchecked Sendable {} +extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} +extension ModuleConfig.MQTTConfig: @unchecked Sendable {} +extension ModuleConfig.MapReportSettings: @unchecked Sendable {} +extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} +extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} +extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} +extension ModuleConfig.AudioConfig: @unchecked Sendable {} +extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} +extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} +extension ModuleConfig.SerialConfig: @unchecked Sendable {} +extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} +extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} +extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} +extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} +extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} +extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} +extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} +extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} +extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} +extension RemoteHardwarePin: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index fc5e37a1..efe6cdd5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope: Sendable { +public struct ServiceEnvelope { // 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope: Sendable { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport: Sendable { +public struct MapReport { // 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. @@ -121,6 +121,11 @@ public struct MapReport: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension ServiceEnvelope: @unchecked Sendable {} +extension MapReport: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index f82b3c51..cf8aa463 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount: Sendable { +public struct Paxcount { // 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. @@ -44,6 +44,10 @@ public struct Paxcount: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Paxcount: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index c5348a8a..c728c961 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum PortNum: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -277,6 +277,11 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -308,9 +313,14 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { .atakForwarder, .max, ] - } +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PortNum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 9c61e6d0..5f51e948 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon: Sendable { +public struct PowerMon { // 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. @@ -31,7 +31,7 @@ public struct PowerMon: Sendable { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum State: SwiftProtobuf.Enum { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,31 +104,37 @@ public struct PowerMon: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] - } public init() {} } +#if swift(>=4.2) + +extension PowerMon.State: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] +} + +#endif // swift(>=4.2) + /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage: Sendable { +public struct PowerStressMessage { // 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. @@ -145,7 +151,7 @@ public struct PowerStressMessage: Sendable { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Opcode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -266,35 +272,48 @@ public struct PowerStressMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] - } public init() {} } +#if swift(>=4.2) + +extension PowerStressMessage.Opcode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PowerMon: @unchecked Sendable {} +extension PowerMon.State: @unchecked Sendable {} +extension PowerStressMessage: @unchecked Sendable {} +extension PowerStressMessage.Opcode: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -304,8 +323,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } public func traverse(visitor: inout V) throws { @@ -360,7 +379,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds.bitPattern != 0 { + if self.numSeconds != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index 60f64504..ac6eeb26 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage: Sendable { +public struct HardwareMessage { // 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. @@ -52,7 +52,7 @@ public struct HardwareMessage: Sendable { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum TypeEnum: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -110,21 +110,32 @@ public struct HardwareMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] - } public init() {} } +#if swift(>=4.2) + +extension HardwareMessage.TypeEnum: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension HardwareMessage: @unchecked Sendable {} +extension HardwareMessage.TypeEnum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index c1f3f678..6fdf3208 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig: Sendable { +public struct RTTTLConfig { // 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. @@ -36,6 +36,10 @@ public struct RTTTLConfig: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension RTTTLConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 0b67eaf6..54efa77b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward: @unchecked Sendable { +public struct StoreAndForward { // 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. @@ -79,7 +79,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable, @unchecked Sendable { + public enum OneOf_Variant: Equatable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,12 +93,38 @@ public struct StoreAndForward: @unchecked Sendable { /// Text from history message. case text(Data) + #if !swift(>=4.1) + public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.stats, .stats): return { + guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.history, .history): return { + guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RequestResponse: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -216,31 +242,11 @@ public struct StoreAndForward: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] - } /// /// TODO: REPLACE - public struct Statistics: Sendable { + public struct Statistics { // 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. @@ -288,7 +294,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public struct History: Sendable { + public struct History { // 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. @@ -313,7 +319,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public struct Heartbeat: Sendable { + public struct Heartbeat { // 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. @@ -334,6 +340,41 @@ public struct StoreAndForward: @unchecked Sendable { public init() {} } +#if swift(>=4.2) + +extension StoreAndForward.RequestResponse: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension StoreAndForward: @unchecked Sendable {} +extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} +extension StoreAndForward.RequestResponse: @unchecked Sendable {} +extension StoreAndForward.Statistics: @unchecked Sendable {} +extension StoreAndForward.History: @unchecked Sendable {} +extension StoreAndForward.Heartbeat: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 7289b713..e4b9ee08 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum TelemetrySensorType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -216,6 +216,11 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -248,12 +253,13 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .icm20948, .max17048, ] - } +#endif // swift(>=4.2) + /// /// Key native device metrics such as battery level -public struct DeviceMetrics: Sendable { +public struct DeviceMetrics { // 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. @@ -326,7 +332,7 @@ public struct DeviceMetrics: Sendable { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics: @unchecked Sendable { +public struct EnvironmentMetrics { // 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. @@ -529,7 +535,7 @@ public struct EnvironmentMetrics: @unchecked Sendable { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics: Sendable { +public struct PowerMetrics { // 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. @@ -614,7 +620,7 @@ public struct PowerMetrics: Sendable { /// /// Air quality metrics -public struct AirQualityMetrics: Sendable { +public struct AirQualityMetrics { // 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. @@ -769,9 +775,53 @@ public struct AirQualityMetrics: Sendable { fileprivate var _particles100Um: UInt32? = nil } +/// +/// Local device mesh statistics +public struct LocalStats { + // 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. + + /// + /// How long the device has been running since the last reboot (in seconds) + public var uptimeSeconds: UInt32 = 0 + + /// + /// Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). + public var channelUtilization: Float = 0 + + /// + /// Percent of airtime for transmission used within the last hour. + public var airUtilTx: Float = 0 + + /// + /// Number of packets sent + public var numPacketsTx: UInt32 = 0 + + /// + /// Number of packets received good + public var numPacketsRx: UInt32 = 0 + + /// + /// Number of packets received that are malformed or violate the protocol + public var numPacketsRxBad: UInt32 = 0 + + /// + /// Number of nodes online (in the past 2 hours) + public var numOnlineNodes: UInt32 = 0 + + /// + /// Number of nodes total + public var numTotalNodes: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry: Sendable { +public struct Telemetry { // 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. @@ -822,9 +872,19 @@ public struct Telemetry: Sendable { set {variant = .powerMetrics(newValue)} } + /// + /// Local device mesh statistics + public var localStats: LocalStats { + get { + if case .localStats(let v)? = variant {return v} + return LocalStats() + } + set {variant = .localStats(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable, Sendable { + public enum OneOf_Variant: Equatable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -837,7 +897,40 @@ public struct Telemetry: Sendable { /// /// Power Metrics case powerMetrics(PowerMetrics) + /// + /// Local device mesh statistics + case localStats(LocalStats) + #if !swift(>=4.1) + public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.deviceMetrics, .deviceMetrics): return { + guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.environmentMetrics, .environmentMetrics): return { + guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.airQualityMetrics, .airQualityMetrics): return { + guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.powerMetrics, .powerMetrics): return { + guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.localStats, .localStats): return { + guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -845,7 +938,7 @@ public struct Telemetry: Sendable { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config: Sendable { +public struct Nau7802Config { // 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. @@ -863,6 +956,18 @@ public struct Nau7802Config: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension TelemetrySensorType: @unchecked Sendable {} +extension DeviceMetrics: @unchecked Sendable {} +extension EnvironmentMetrics: @unchecked Sendable {} +extension PowerMetrics: @unchecked Sendable {} +extension AirQualityMetrics: @unchecked Sendable {} +extension LocalStats: @unchecked Sendable {} +extension Telemetry: @unchecked Sendable {} +extension Telemetry.OneOf_Variant: @unchecked Sendable {} +extension Nau7802Config: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1333,6 +1438,80 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem } } +extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LocalStats" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "uptime_seconds"), + 2: .standard(proto: "channel_utilization"), + 3: .standard(proto: "air_util_tx"), + 4: .standard(proto: "num_packets_tx"), + 5: .standard(proto: "num_packets_rx"), + 6: .standard(proto: "num_packets_rx_bad"), + 7: .standard(proto: "num_online_nodes"), + 8: .standard(proto: "num_total_nodes"), + ] + + 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.decodeSingularFloatField(value: &self.channelUtilization) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.airUtilTx) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsTx) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsRx) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsRxBad) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineNodes) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.numTotalNodes) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.uptimeSeconds != 0 { + try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) + } + if self.channelUtilization != 0 { + try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 2) + } + if self.airUtilTx != 0 { + try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 3) + } + if self.numPacketsTx != 0 { + try visitor.visitSingularUInt32Field(value: self.numPacketsTx, fieldNumber: 4) + } + if self.numPacketsRx != 0 { + try visitor.visitSingularUInt32Field(value: self.numPacketsRx, fieldNumber: 5) + } + if self.numPacketsRxBad != 0 { + try visitor.visitSingularUInt32Field(value: self.numPacketsRxBad, fieldNumber: 6) + } + if self.numOnlineNodes != 0 { + try visitor.visitSingularUInt32Field(value: self.numOnlineNodes, fieldNumber: 7) + } + if self.numTotalNodes != 0 { + try visitor.visitSingularUInt32Field(value: self.numTotalNodes, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: LocalStats, rhs: LocalStats) -> Bool { + if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs.channelUtilization != rhs.channelUtilization {return false} + if lhs.airUtilTx != rhs.airUtilTx {return false} + if lhs.numPacketsTx != rhs.numPacketsTx {return false} + if lhs.numPacketsRx != rhs.numPacketsRx {return false} + if lhs.numPacketsRxBad != rhs.numPacketsRxBad {return false} + if lhs.numOnlineNodes != rhs.numOnlineNodes {return false} + if lhs.numTotalNodes != rhs.numTotalNodes {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 = [ @@ -1341,6 +1520,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 3: .standard(proto: "environment_metrics"), 4: .standard(proto: "air_quality_metrics"), 5: .standard(proto: "power_metrics"), + 6: .standard(proto: "local_stats"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1402,6 +1582,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .powerMetrics(v) } }() + case 6: try { + var v: LocalStats? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .localStats(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .localStats(v) + } + }() default: break } } @@ -1432,6 +1625,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .powerMetrics(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 5) }() + case .localStats?: try { + guard case .localStats(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1469,7 +1666,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor.bitPattern != 0 { + if self.calibrationFactor != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 89d0097c..1f41fe0b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem: @unchecked Sendable { +public struct XModem { // 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. @@ -35,7 +35,7 @@ public struct XModem: @unchecked Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Control: SwiftProtobuf.Enum { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,23 +79,34 @@ public struct XModem: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] - } public init() {} } +#if swift(>=4.2) + +extension XModem.Control: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension XModem: @unchecked Sendable {} +extension XModem.Control: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/protobufs b/protobufs index 3e753697..59d035a3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 3e753697aa1140d2c998cb63739729e733002874 +Subproject commit 59d035a37dbeadb28db97acce5f738ba52ee9d3a From bb4967fbf419696f6bedaa3f24a5a73e42d546fc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 17 Aug 2024 13:12:13 -0700 Subject: [PATCH 120/333] Use new set time method after want config is complete --- Meshtastic/Helpers/BLEManager.swift | 25 +++++++++++ .../Weather/LocalWeatherConditions.swift | 18 ++++---- .../Nodes/Helpers/Map/PositionPopover.swift | 31 +++++++------- .../Views/Nodes/Helpers/NodeListItem.swift | 4 +- Meshtastic/Views/Nodes/MeshMap.swift | 25 ++++++----- .../Sources/meshtastic/mesh.pb.swift | 42 ++++++++++++++++++- .../Sources/meshtastic/module_config.pb.swift | 2 +- .../Sources/meshtastic/portnums.pb.swift | 2 +- protobufs | 2 +- 9 files changed, 105 insertions(+), 46 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4e7489dc..7e08ccfa 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -893,6 +893,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)") + sendTime() peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again @@ -1298,6 +1299,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } + public func sendTime() -> Bool { + var adminPacket = AdminMessage() + adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = 0 + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 8f057610..27675141 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -101,7 +101,7 @@ struct WeatherConditionsCompactWidget: View { .font(temperature.length < 4 ? .system(size: 90) : .system(size: 60) ) } .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -123,7 +123,7 @@ struct HumidityCompactWidget: View { } .padding(.horizontal) .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -135,16 +135,15 @@ struct PressureCompactWidget: View { var body: some View { VStack(alignment: .leading) { Label { Text("PRESSURE") } icon: { Image(systemName: "gauge").symbolRenderingMode(.multicolor) } - .font(.caption2) + .font(.callout) Text(pressure) .font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) ) Text(low ? "LOW" : "HIGH") .padding(.bottom) Text(unit) } - .padding(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + .padding(.horizontal, 5) + .frame(width: 175, height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -156,17 +155,14 @@ struct WindCompactWidget: View { var body: some View { VStack(alignment: .leading) { Label { Text("WIND") } icon: { Image(systemName: "wind").foregroundColor(.accentColor) } - .font(.caption) Text("\(direction)") - .font(.caption) .padding(.bottom, 10) Text(speed) .font(.system(size: 35)) Text("Gusts \(gust)") } - .padding(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + //.padding(.horizontal) + .frame(width: 175, height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 0c708058..6a717629 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -154,6 +154,22 @@ struct PositionPopover: View { .rotationEffect(degrees) } .padding(.bottom, 5) + /// Distance + if let lastLocation = locationsHandler.locationsArray.last { + /// Distance + if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + } + } /// Speed let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) Label { @@ -179,21 +195,6 @@ struct PositionPopover: View { } .padding(.bottom, 5) } - if let lastLocation = locationsHandler.locationsArray.last { - /// Distance - if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) - Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") - .foregroundColor(.primary) - .font(idiom == .phone ? .callout : .body) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - } - } Spacer() } Spacer() diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 5d46806c..3013e8df 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -98,7 +98,7 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) let headingDegrees = Angle.degrees(trueBearing) Image(systemName: "location.north") .font(.callout) @@ -124,7 +124,7 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.secondary) - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) let headingDegrees = Angle.degrees(trueBearing) Image(systemName: "location.north") .font(.callout) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 31923562..9e09a0c0 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -62,7 +62,6 @@ struct MeshMap: View { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint) - } .mapScope(mapScope) .mapStyle(mapStyle) @@ -159,6 +158,7 @@ struct MeshMap: View { } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { + Spacer() Button(action: { withAnimation { editingSettings = !editingSettings @@ -170,18 +170,17 @@ struct MeshMap: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - Spacer() - Button(action: { - withAnimation { - editingFilters = !editingFilters - } - }) { - Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) +// Button(action: { +// withAnimation { +// editingFilters = !editingFilters +// } +// }) { +// Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") +// .padding(.vertical, 5) +// } +// .tint(Color(UIColor.secondarySystemBackground)) +// .foregroundColor(.accentColor) +// .buttonStyle(.borderedProminent) } .controlSize(.regular) .padding(5) diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 1f4c60a5..ba12908d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -342,6 +342,10 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// SSD1306 OLED and No GPS case radiomaster900Bandit // = 74 + /// + /// Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. + case me25Ls014Y10Td // = 75 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -429,6 +433,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 72: self = .rak3172 case 73: self = .wioE5 case 74: self = .radiomaster900Bandit + case 75: self = .me25Ls014Y10Td case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -510,6 +515,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .rak3172: return 72 case .wioE5: return 73 case .radiomaster900Bandit: return 74 + case .me25Ls014Y10Td: return 75 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -596,6 +602,7 @@ extension HardwareModel: CaseIterable { .rak3172, .wioE5, .radiomaster900Bandit, + .me25Ls014Y10Td, .privateHw, ] } @@ -1219,16 +1226,28 @@ public struct User { } /// -/// A message used in our Dynamic Source Routing protocol (RFC 4728 based) +/// A message used in a traceroute public struct RouteDiscovery { // 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. /// - /// The list of nodenums this packet has visited so far + /// The list of nodenums this packet has visited so far to the destination. public var route: [UInt32] = [] + /// + /// The list of SNRs (in dB, scaled by 4) in the route towards the destination. + public var snrTowards: [Int32] = [] + + /// + /// The list of nodenums the packet has visited on the way back from the destination. + public var routeBack: [UInt32] = [] + + /// + /// The list of SNRs (in dB, scaled by 4) in the route back from the destination. + public var snrBack: [Int32] = [] + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -3221,6 +3240,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 72: .same(proto: "RAK3172"), 73: .same(proto: "WIO_E5"), 74: .same(proto: "RADIOMASTER_900_BANDIT"), + 75: .same(proto: "ME25LS01_4Y10TD"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3600,6 +3620,9 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement public static let protoMessageName: String = _protobuf_package + ".RouteDiscovery" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "route"), + 2: .standard(proto: "snr_towards"), + 3: .standard(proto: "route_back"), + 4: .standard(proto: "snr_back"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3609,6 +3632,9 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeRepeatedFixed32Field(value: &self.route) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.snrTowards) }() + case 3: try { try decoder.decodeRepeatedFixed32Field(value: &self.routeBack) }() + case 4: try { try decoder.decodeRepeatedInt32Field(value: &self.snrBack) }() default: break } } @@ -3618,11 +3644,23 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if !self.route.isEmpty { try visitor.visitPackedFixed32Field(value: self.route, fieldNumber: 1) } + if !self.snrTowards.isEmpty { + try visitor.visitPackedInt32Field(value: self.snrTowards, fieldNumber: 2) + } + if !self.routeBack.isEmpty { + try visitor.visitPackedFixed32Field(value: self.routeBack, fieldNumber: 3) + } + if !self.snrBack.isEmpty { + try visitor.visitPackedInt32Field(value: self.snrBack, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: RouteDiscovery, rhs: RouteDiscovery) -> Bool { if lhs.route != rhs.route {return false} + if lhs.snrTowards != rhs.snrTowards {return false} + if lhs.routeBack != rhs.routeBack {return false} + if lhs.snrBack != rhs.snrBack {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index c68ffd83..3186c349 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -1044,7 +1044,7 @@ public struct ModuleConfig { /// /// Input event origin accepted by the canned message module. - /// Can be e.g. "rotEnc1", "upDownEnc1" or keyword "_any" + /// Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any" public var allowInputSource: String = String() /// diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index c728c961..dd7e036f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -167,7 +167,7 @@ public enum PortNum: SwiftProtobuf.Enum { /// /// Provides a traceroute functionality to show the route a packet towards - /// a certain destination would take on the mesh. + /// a certain destination would take on the mesh. Contains a RouteDiscovery message as payload. /// ENCODING: Protobuf case tracerouteApp // = 70 diff --git a/protobufs b/protobufs index 59d035a3..b6237629 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 59d035a37dbeadb28db97acce5f738ba52ee9d3a +Subproject commit b623762940ebdb1887a3b31b86f4d9cdaa7e6ecf From b260b644e41580acc10dbb1953ee99887b57cf49 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 17 Aug 2024 15:31:35 -0700 Subject: [PATCH 121/333] Fix node list battery level update bug --- Meshtastic.xcodeproj/project.pbxproj | 4 --- .../CoreData/NodeInfoEntityExtension.swift | 4 +++ Meshtastic/Views/Helpers/BatteryCompact.swift | 2 +- .../Views/Helpers/BatteryLevelCompact.swift | 25 ------------------- .../Views/Nodes/Helpers/NodeListItem.swift | 6 +++-- 5 files changed, 9 insertions(+), 32 deletions(-) delete mode 100644 Meshtastic/Views/Helpers/BatteryLevelCompact.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2c59e43b..7d5f9023 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -152,7 +152,6 @@ DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; }; DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; }; - DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */; }; DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F40F2A9EE5B400230ECE /* Messages.swift */; }; DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4112A9EE5DD00230ECE /* UserList.swift */; }; DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4132A9EE5F000230ECE /* ChannelList.swift */; }; @@ -418,7 +417,6 @@ DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = ""; }; DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = ""; }; DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrength.swift; sourceTree = ""; }; - DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevelCompact.swift; sourceTree = ""; }; DDB8F40F2A9EE5B400230ECE /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; DDB8F4112A9EE5DD00230ECE /* UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserList.swift; sourceTree = ""; }; DDB8F4132A9EE5F000230ECE /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = ""; }; @@ -921,7 +919,6 @@ DD6F65772C6EAB860053C113 /* Help */, DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, - DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, @@ -1310,7 +1307,6 @@ DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */, DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */, - DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 66c915df..eb0086c7 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -14,6 +14,10 @@ extension NodeInfoEntity { return self.positions?.lastObject as? PositionEntity } + var latestDeviceMetrics: TelemetryEntity? { + return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity + } + var latestEnvironmentMetrics: TelemetryEntity? { return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 28f62a57..bc714da6 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -7,7 +7,7 @@ import SwiftUI struct BatteryCompact: View { - @State var batteryLevel: Int32 + var batteryLevel: Int32 var font: Font var iconFont: Font var color: Color diff --git a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift deleted file mode 100644 index e1354e23..00000000 --- a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// BatteryIcon.swift -// Meshtastic -// -// Copyright Garth Vander Houwen 3/24/23. -// -import SwiftUI - -struct BatteryLevelCompact: View { - - @ObservedObject var node: NodeInfoEntity - - var font: Font - var iconFont: Font - var color: Color - - var body: some View { - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - let batteryLevel = mostRecent?.batteryLevel ?? 0 - if deviceMetrics?.count ?? 0 > 0 { - BatteryCompact(batteryLevel: batteryLevel, font: font, iconFont: iconFont, color: color) - } - } -} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 3013e8df..63aff338 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -23,8 +23,10 @@ struct NodeListItem: View { VStack(alignment: .leading) { CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) .padding(.trailing, 5) - BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) - .padding(.trailing, 5) + if node.latestDeviceMetrics != nil { + BatteryCompact(batteryLevel: node.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor) + .padding(.trailing, 5) + } } VStack(alignment: .leading) { HStack { From fe9e0d73b8d6f3aef990288326c3395ec08641eb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 17 Aug 2024 15:36:23 -0700 Subject: [PATCH 122/333] Add firmware version requirement to encryption help --- Meshtastic/Views/Helpers/Help/LockLegend.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift index 9c5d216b..36716798 100644 --- a/Meshtastic/Views/Helpers/Help/LockLegend.swift +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -34,7 +34,7 @@ struct LockLegend: View { Text("Public Key Encryption") .fontWeight(.semibold) } - Text("Direct messages are using the new public key infrastructure for encryption.") + Text("Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater.") .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .font(.callout) .fixedSize(horizontal: false, vertical: true) From ec704e2c2b4e38b5a3520a664b7f5b41661df207 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 08:36:30 -0700 Subject: [PATCH 123/333] Admin message session passkeys --- Localizable.xcstrings | 2 +- Meshtastic/Helpers/BLEManager.swift | 181 ++++++++++++++---- .../contents | 2 + Meshtastic/Persistence/UpdateCoreData.swift | 133 +++++++------ 4 files changed, 222 insertions(+), 96 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 16d61fea..21466cc5 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6316,7 +6316,7 @@ "Direct Message Help" : { }, - "Direct messages are using the new public key infrastructure for encryption." : { + "Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater." : { }, "Direct messages are using the shared key for the channel." : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 7e08ccfa..49e43d54 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -392,6 +392,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getDeviceMetadataRequest = true + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1351,6 +1357,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendReboot(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1376,6 +1385,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1401,6 +1413,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendEnterDfuMode(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.enterDfuModeRequest = true + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1427,6 +1442,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.factoryResetConfig = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1452,6 +1470,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.nodedbReset = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = 0 // UInt32(fromUser.num) @@ -1664,6 +1685,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1786,6 +1810,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1809,6 +1836,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveBluetoothConfig(config: Config.BluetoothConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.bluetooth = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1826,7 +1856,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } @@ -1837,7 +1867,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.device = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1854,7 +1886,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } return 0 @@ -1863,6 +1895,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1881,7 +1916,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } return 0 @@ -1891,6 +1926,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.lora = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1908,7 +1946,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey,context: context) return Int64(meshPacket.id) } @@ -1919,7 +1957,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.position = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1981,7 +2021,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.network = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2012,7 +2054,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.security = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2043,7 +2087,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setModuleConfig.ambientLighting = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2073,7 +2119,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2103,7 +2151,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2134,7 +2184,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setModuleConfig.detectionSensor = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index ba391495..009b9d20 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -397,7 +397,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } } -func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.bluetooth.config %@".localized, String(nodeNum)) MeshLogger.log("📶 \(logString)") @@ -422,6 +422,9 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin) fetchedNode[0].bluetoothConfig?.deviceLoggingEnabled = config.deviceLoggingEnabled } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [BluetoothConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -439,7 +442,7 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, } } -func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.device.config %@".localized, String(nodeNum)) MeshLogger.log("📟 \(logString)") @@ -477,6 +480,9 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, conte fetchedNode[0].deviceConfig?.isManaged = config.isManaged fetchedNode[0].deviceConfig?.tzdef = config.tzdef } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)") @@ -492,7 +498,7 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, conte } } -func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.display.config %@".localized, nodeNum.toHex()) MeshLogger.log("🖥️ \(logString)") @@ -518,7 +524,6 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, con newDisplayConfig.units = Int32(config.units.rawValue) newDisplayConfig.headingBold = config.headingBold fetchedNode[0].displayConfig = newDisplayConfig - } else { fetchedNode[0].displayConfig?.gpsFormat = Int32(config.gpsFormat.rawValue) @@ -531,7 +536,9 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, con fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) fetchedNode[0].displayConfig?.headingBold = config.headingBold } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() @@ -556,7 +563,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, con } } -func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.lora.config %@".localized, nodeNum.toHex()) MeshLogger.log("📻 \(logString)") @@ -604,6 +611,9 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -621,7 +631,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: } } -func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.network.config %@".localized, String(nodeNum)) MeshLogger.log("🌐 \(logString)") @@ -646,7 +656,9 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, con fetchedNode[0].networkConfig?.wifiSsid = config.wifiSsid fetchedNode[0].networkConfig?.wifiPsk = config.wifiPsk } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -665,7 +677,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, con } } -func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.position.config %@".localized, String(nodeNum)) MeshLogger.log("🗺️ \(logString)") @@ -708,6 +720,9 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, c fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -725,7 +740,7 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, c } } -func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.power.config %@".localized, String(nodeNum)) MeshLogger.log("🗺️ \(logString)") @@ -755,6 +770,9 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context fetchedNode[0].powerConfig?.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs) fetchedNode[0].powerConfig?.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -772,7 +790,7 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context } } -func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum)) MeshLogger.log("🛡️ \(logString)") @@ -803,7 +821,9 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, c fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -822,7 +842,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, c } } -func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum)) MeshLogger.log("🏮 \(logString)") @@ -836,16 +856,13 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin if !fetchedNode.isEmpty { if fetchedNode[0].cannedMessageConfig == nil { - let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context) - newAmbientLightingConfig.ledState = config.ledState newAmbientLightingConfig.current = Int32(config.current) newAmbientLightingConfig.red = Int32(config.red) newAmbientLightingConfig.green = Int32(config.green) newAmbientLightingConfig.blue = Int32(config.blue) fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig - } else { if fetchedNode[0].ambientLightingConfig == nil { @@ -857,7 +874,9 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin fetchedNode[0].ambientLightingConfig?.green = Int32(config.green) fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -875,7 +894,7 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin } } -func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum)) MeshLogger.log("🥫 \(logString)") @@ -889,9 +908,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo if !fetchedNode.isEmpty { if fetchedNode[0].cannedMessageConfig == nil { - let newCannedMessageConfig = CannedMessageConfigEntity(context: context) - newCannedMessageConfig.enabled = config.enabled newCannedMessageConfig.sendBell = config.sendBell newCannedMessageConfig.rotary1Enabled = config.rotary1Enabled @@ -902,11 +919,8 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo newCannedMessageConfig.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue) newCannedMessageConfig.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) newCannedMessageConfig.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) - fetchedNode[0].cannedMessageConfig = newCannedMessageConfig - } else { - fetchedNode[0].cannedMessageConfig?.enabled = config.enabled fetchedNode[0].cannedMessageConfig?.sendBell = config.sendBell fetchedNode[0].cannedMessageConfig?.rotary1Enabled = config.rotary1Enabled @@ -918,7 +932,9 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo fetchedNode[0].cannedMessageConfig?.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) fetchedNode[0].cannedMessageConfig?.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -936,7 +952,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo } } -func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum)) MeshLogger.log("🕵️ \(logString)") @@ -948,21 +964,17 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Detection Sensor Config if !fetchedNode.isEmpty { - if fetchedNode[0].detectionSensorConfig == nil { - let newConfig = DetectionSensorConfigEntity(context: context) newConfig.enabled = config.enabled newConfig.sendBell = config.sendBell newConfig.name = config.name - newConfig.monitorPin = Int32(config.monitorPin) newConfig.detectionTriggeredHigh = config.detectionTriggeredHigh newConfig.usePullup = config.usePullup newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) fetchedNode[0].detectionSensorConfig = newConfig - } else { fetchedNode[0].detectionSensorConfig?.enabled = config.enabled fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell @@ -973,7 +985,9 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -994,7 +1008,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso } } -func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.externalnotification.config %@".localized, String(nodeNum)) MeshLogger.log("📣 \(logString)") @@ -1025,7 +1039,6 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout) newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig - } else { fetchedNode[0].externalNotificationConfig?.enabled = config.enabled fetchedNode[0].externalNotificationConfig?.usePWM = config.usePwm @@ -1043,7 +1056,9 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout) fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1061,7 +1076,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN } } -func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum)) MeshLogger.log("🧑‍🤝‍🧑 \(logString)") @@ -1073,19 +1088,18 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save PAX Counter Config if !fetchedNode.isEmpty { - if fetchedNode[0].paxCounterConfig == nil { let newPaxCounterConfig = PaxCounterConfigEntity(context: context) newPaxCounterConfig.enabled = config.enabled newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval) - fetchedNode[0].paxCounterConfig = newPaxCounterConfig - } else { fetchedNode[0].paxCounterConfig?.enabled = config.enabled fetchedNode[0].paxCounterConfig?.updateInterval = Int32(config.paxcounterUpdateInterval) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") @@ -1103,7 +1117,7 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n } } -func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.ringtone.config %@".localized, String(nodeNum)) MeshLogger.log("⛰️ \(logString)") @@ -1122,6 +1136,9 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManage } else { fetchedNode[0].rtttlConfig?.ringtone = ringtone } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1139,7 +1156,7 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManage } } -func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.mqtt.config %@".localized, String(nodeNum)) MeshLogger.log("🌉 \(logString)") @@ -1151,7 +1168,6 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save MQTT Config if !fetchedNode.isEmpty { - if fetchedNode[0].mqttConfig == nil { let newMQTTConfig = MQTTConfigEntity(context: context) newMQTTConfig.enabled = config.enabled @@ -1181,6 +1197,9 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") @@ -1198,7 +1217,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 } } -func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.rangetest.config %@".localized, String(nodeNum)) MeshLogger.log("⛰️ \(logString)") @@ -1221,6 +1240,9 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod fetchedNode[0].rangeTestConfig?.enabled = config.enabled fetchedNode[0].rangeTestConfig?.save = config.save } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1238,7 +1260,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod } } -func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.serial.config %@".localized, String(nodeNum)) MeshLogger.log("🤖 \(logString)") @@ -1250,9 +1272,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { - if fetchedNode[0].serialConfig == nil { - let newSerialConfig = SerialConfigEntity(context: context) newSerialConfig.enabled = config.enabled newSerialConfig.echo = config.echo @@ -1262,7 +1282,6 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: newSerialConfig.timeout = Int32(config.timeout) newSerialConfig.mode = Int32(config.mode.rawValue) fetchedNode[0].serialConfig = newSerialConfig - } else { fetchedNode[0].serialConfig?.enabled = config.enabled fetchedNode[0].serialConfig?.echo = config.echo @@ -1272,7 +1291,9 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: fetchedNode[0].serialConfig?.timeout = Int32(config.timeout) fetchedNode[0].serialConfig?.mode = Int32(config.mode.rawValue) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -1293,7 +1314,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: } } -func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.storeforward.config %@".localized, String(nodeNum)) MeshLogger.log("📬 \(logString)") @@ -1305,9 +1326,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Store & Forward Sensor Config if !fetchedNode.isEmpty { - if fetchedNode[0].storeForwardConfig == nil { - let newConfig = StoreForwardConfigEntity(context: context) newConfig.enabled = config.enabled newConfig.heartbeat = config.heartbeat @@ -1315,7 +1334,6 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi newConfig.historyReturnMax = Int32(config.historyReturnMax) newConfig.historyReturnWindow = Int32(config.historyReturnWindow) fetchedNode[0].storeForwardConfig = newConfig - } else { fetchedNode[0].storeForwardConfig?.enabled = config.enabled fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat @@ -1323,6 +1341,9 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax) fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1340,21 +1361,18 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi } } -func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.telemetry.config %@".localized, String(nodeNum)) MeshLogger.log("📈 \(logString)") let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Telemetry Config if !fetchedNode.isEmpty { - if fetchedNode[0].telemetryConfig == nil { - let newTelemetryConfig = TelemetryConfigEntity(context: context) newTelemetryConfig.deviceUpdateInterval = Int32(config.deviceUpdateInterval) newTelemetryConfig.environmentUpdateInterval = Int32(config.environmentUpdateInterval) @@ -1365,7 +1383,6 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod newTelemetryConfig.powerUpdateInterval = Int32(config.powerUpdateInterval) newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig - } else { fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(config.deviceUpdateInterval) fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(config.environmentUpdateInterval) @@ -1376,7 +1393,9 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(config.powerUpdateInterval) fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + } do { try context.save() Logger.data.info("💾 [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)") From 45d4cb64f5a8cb7d8153831b96bb3eea3892a1f4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 08:44:33 -0700 Subject: [PATCH 124/333] Allow saving of admin key --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index d031306b..642a8fb7 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -131,7 +131,7 @@ struct SecurityConfig: View { var config = Config.SecurityConfig() //config.publicKey = publicKey //config.privateKey = privateKey - //config.adminKey = adminKey + config.adminKey = Data(base64Encoded: adminKey) ?? Data() config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled From 9f5414159ba3db2ed59d5d0e0f35a93c08174061 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 08:53:59 -0700 Subject: [PATCH 125/333] Add timeout field for session passcode --- Meshtastic/Persistence/UpdateCoreData.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 009b9d20..98acc96b 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -424,6 +424,7 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -482,6 +483,7 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -538,6 +540,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { @@ -613,6 +616,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -658,6 +662,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -722,6 +727,7 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, s } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -772,6 +778,7 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, session } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -823,6 +830,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -876,6 +884,7 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -934,6 +943,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -987,6 +997,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1058,6 +1069,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1099,6 +1111,7 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1138,6 +1151,7 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: D } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1199,6 +1213,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1242,6 +1257,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1293,6 +1309,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1343,6 +1360,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -1395,6 +1413,7 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() From e44003907cc3472847463c68e65a982f690e842b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 09:09:45 -0700 Subject: [PATCH 126/333] moor retry --- Meshtastic/Enums/RoutingError.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 3af6e9b2..108fedd2 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -81,9 +81,9 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .noInterface: return true case .maxRetransmit: - return false + return true case .noChannel: - return false + return true case .tooLarge: return false case .noResponse: From 9b50626ebe0a3141180771becde45902b4754454 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 10:00:15 -0700 Subject: [PATCH 127/333] Clean up admin drop down --- .../CoreData/NodeInfoEntityExtension.swift | 9 +++++++++ .../Views/Settings/Config/SecurityConfig.swift | 3 +++ Meshtastic/Views/Settings/Settings.swift | 13 ++++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index eb0086c7..c7a03e33 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -58,6 +58,15 @@ extension NodeInfoEntity { } return false } + + var canRemoteAdmin: Bool { + if !(securityConfig?.adminKey?.isEmpty ?? true) { + return true + } else { + let adminChannel = myInfo?.channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } + return adminChannel?.count ?? 0 > 0 + } + } } public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 642a8fb7..81672a15 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -120,6 +120,9 @@ struct SecurityConfig: View { .onChange(of: adminChannelEnabled) { if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } + .onChange(of: adminKey) { _ in + hasChanges = true + } SaveConfigButton(node: node, hasChanges: $hasChanges) { guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 85aafd8f..3bf91f3c 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -348,13 +348,13 @@ struct Settings: View { if !(node?.deviceConfig?.isManaged ?? false) { if bleManager.connectedPeripheral != nil { Section("Configure") { - if hasAdmin { + if node?.canRemoteAdmin ?? false { Picker("Configuring Node", selection: $selectedNode) { if selectedNode == 0 { Text("Connect to a Node").tag(0) } - ForEach(nodes) { node in + /// Connected Node if node.num == bleManager.connectedPeripheral?.num ?? 0 { Label { Text("BLE: \(node.user?.longName ?? "unknown".localized)") @@ -362,7 +362,14 @@ struct Settings: View { Image(systemName: "antenna.radiowaves.left.and.right") } .tag(Int(node.num)) - } else if node.metadata != nil { + } else if node.canRemoteAdmin { /// Nodes using the new PKI system + Label { + Text("Remote: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "av.remote") + } + .tag(Int(node.num)) + } else if node.metadata != nil { /// Nodes using the old admin system Label { Text("Remote: \(node.user?.longName ?? "unknown".localized)") } icon: { From aad3bd4f891045887d87a7ab88b6b382ff722b57 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 11:39:41 -0700 Subject: [PATCH 128/333] Pass keys around better --- Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift | 2 +- Meshtastic/Views/Settings/Config/NetworkConfig.swift | 1 - Meshtastic/Views/Settings/Config/SecurityConfig.swift | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index c7a03e33..7c7a3e27 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -58,7 +58,7 @@ extension NodeInfoEntity { } return false } - + var canRemoteAdmin: Bool { if !(securityConfig?.adminKey?.isEmpty ?? true) { return true diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 51981606..0554c766 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -133,7 +133,6 @@ struct NetworkConfig: View { } .onChange(of: wifiSsid) { newSSID in if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true } - } .onChange(of: wifiPsk) { newPsk in if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 81672a15..6e4c1168 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -132,8 +132,8 @@ struct SecurityConfig: View { } var config = Config.SecurityConfig() - //config.publicKey = publicKey - //config.privateKey = privateKey + config.publicKey = Data(base64Encoded: publicKey) ?? Data() + config.privateKey = Data(base64Encoded: privateKey) ?? Data() config.adminKey = Data(base64Encoded: adminKey) ?? Data() config.isManaged = isManaged config.serialEnabled = serialEnabled From 8b2bc246dd328d21eeb363af5524277f7ff7cdd7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 11:43:42 -0700 Subject: [PATCH 129/333] Add change events for all keys --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 6e4c1168..94f3da04 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -120,6 +120,12 @@ struct SecurityConfig: View { .onChange(of: adminChannelEnabled) { if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } + .onChange(of: publicKey) { _ in + hasChanges = true + } + .onChange(of: privateKey) { _ in + hasChanges = true + } .onChange(of: adminKey) { _ in hasChanges = true } From dee96175bf3fc12bb035c0794974305d59d8829d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 17:15:55 -0700 Subject: [PATCH 130/333] Update admin drop down --- .../CoreData/NodeInfoEntityExtension.swift | 2 +- Meshtastic/Extensions/UserDefaults.swift | 4 +++ Meshtastic/Views/Settings/AppSettings.swift | 5 ++++ .../Views/Settings/SaveChannelQRCode.swift | 25 +++++++++---------- Meshtastic/Views/Settings/Settings.swift | 4 +-- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 7c7a3e27..07ee9117 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -60,7 +60,7 @@ extension NodeInfoEntity { } var canRemoteAdmin: Bool { - if !(securityConfig?.adminKey?.isEmpty ?? true) { + if UserDefaults.enableAdministration { return true } else { let adminChannel = myInfo?.channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index ca8441be..740f04e2 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -71,6 +71,7 @@ extension UserDefaults { case modemPreset case firmwareVersion case environmentEnableWeatherKit + case enableAdministration case testIntEnum } @@ -162,6 +163,9 @@ extension UserDefaults { @UserDefault(.environmentEnableWeatherKit, defaultValue: true) static var environmentEnableWeatherKit: Bool + @UserDefault(.enableAdministration, defaultValue: false) + static var enableAdministration: Bool + @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index f9f46cbc..ef3bc384 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -13,6 +13,7 @@ struct AppSettings: View { @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false @AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true + @AppStorage("enableAdministration") private var enableAdministration: Bool = false var body: some View { VStack { Form { @@ -23,6 +24,10 @@ struct AppSettings: View { UIApplication.shared.open(url) } } + Toggle(isOn: $enableAdministration) { + Label("Administration", systemImage: "gearshape.2") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("environment")) { VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index fee7877b..ac8138fa 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -50,6 +50,18 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() .disabled(!connectedToDevice) +#if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("cancel", systemImage: "xmark") + + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() +#endif } else { Button { dismiss() @@ -62,19 +74,6 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() } - - #if targetEnvironment(macCatalyst) - Button { - dismiss() - } label: { - Label("cancel", systemImage: "xmark") - - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - #endif } } .onAppear { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 3bf91f3c..510e56d3 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -362,7 +362,7 @@ struct Settings: View { Image(systemName: "antenna.radiowaves.left.and.right") } .tag(Int(node.num)) - } else if node.canRemoteAdmin { /// Nodes using the new PKI system + } else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { /// Nodes using the new PKI system Label { Text("Remote: \(node.user?.longName ?? "unknown".localized)") } icon: { @@ -376,7 +376,7 @@ struct Settings: View { Image(systemName: "av.remote") } .tag(Int(node.num)) - } else if hasAdmin { + } else if hasAdmin || node.user?.pkiEncrypted ?? false { Label { Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { From 758ab6315a04b15457161cda50955a3b1538d7b0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 18 Aug 2024 17:38:13 -0700 Subject: [PATCH 131/333] allow request metadata for all unmanaged nodes --- Meshtastic/Views/Settings/Settings.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 510e56d3..7bc3643c 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -343,8 +343,6 @@ struct Settings: View { } } - let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 - if !(node?.deviceConfig?.isManaged ?? false) { if bleManager.connectedPeripheral != nil { Section("Configure") { @@ -376,7 +374,7 @@ struct Settings: View { Image(systemName: "av.remote") } .tag(Int(node.num)) - } else if hasAdmin || node.user?.pkiEncrypted ?? false { + } else { Label { Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { From 10157ed5477704da2caff8f884c90cc9548e3945 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 19 Aug 2024 00:27:26 -0700 Subject: [PATCH 132/333] Update admin node list sort to be less infurating --- Meshtastic/Helpers/BLEManager.swift | 32 +++++++++++++++++++ .../Settings/Config/SecurityConfig.swift | 9 ++++++ Meshtastic/Views/Settings/Settings.swift | 2 ++ 3 files changed, 43 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 49e43d54..4280533a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2766,6 +2766,38 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func requestSecurityConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getConfigRequest = AdminMessage.ConfigType.securityConfig + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 94f3da04..caf64e57 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -129,6 +129,15 @@ struct SecurityConfig: View { .onChange(of: adminKey) { _ in hasChanges = true } + .onFirstAppear { + // Need to request a Power config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.securityConfig == nil { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestSecurityConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } SaveConfigButton(node: node, hasChanges: $hasChanges) { guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 7bc3643c..b28cdd21 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -17,6 +17,8 @@ struct Settings: View { @FetchRequest( sortDescriptors: [ NSSortDescriptor(key: "favorite", ascending: false), + NSSortDescriptor(key: "user.pkiEncrypted", ascending: false), + NSSortDescriptor(key: "viaMqtt", ascending: true), NSSortDescriptor(key: "user.longName", ascending: true) ], animation: .default From 0ff613ec68d93f42ce6053163a07b656fad3ed5e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 19 Aug 2024 09:30:21 -0700 Subject: [PATCH 133/333] dont send session passkey with requests --- Meshtastic/Helpers/BLEManager.swift | 64 +---------------------------- 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4280533a..326f7966 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -392,9 +392,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getDeviceMetadataRequest = true - if fromUser != toUser { - adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() - } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Mon, 19 Aug 2024 12:30:26 -0700 Subject: [PATCH 134/333] Agressive metadata requests --- Localizable.xcstrings | 6 ++++++ Meshtastic/Helpers/MeshPackets.swift | 10 +++++++--- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift | 6 ++++++ Meshtastic/Views/Settings/Settings.swift | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 21466cc5..b9858fe8 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -22561,6 +22561,12 @@ }, "WIND" : { + }, + "Wind Direction" : { + + }, + "Wind Speed" : { + }, "x" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 87ada50b..d3bef97a 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -198,7 +198,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo } } -func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NSManagedObjectContext) { +func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { if metadata.isInitialized { let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex()) @@ -232,6 +232,10 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS newNode.metadata = newMetadata } } + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() } catch { @@ -488,7 +492,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { - deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context) + deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { let config = adminMessage.getConfigResponse if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { @@ -506,7 +510,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { - upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), context: context) + upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { let moduleConfig = adminMessage.getModuleConfigResponse diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 98acc96b..6f2520ee 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -828,7 +828,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled } - if sessionPasskey != nil { + if sessionPasskey?.count != 0 { fetchedNode[0].sessionPasskey = sessionPasskey fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index f0d2dd04..571511cf 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -93,6 +93,12 @@ struct EnvironmentMetricsLog: View { IndoorAirQuality(iaq: Int(em.iaq), displayMode: IaqDisplayMode.dot ) } } + TableColumn("Wind Speed") { em in + Text("\(String(format: "%.1f", em.windSpeed)) hPa") + } + TableColumn("Wind Direction") { em in + Text("\(String(format: "%.1f", em.windDirection)) hPa") + } TableColumn("timestamp") { em in Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index b28cdd21..4bf75555 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -393,7 +393,7 @@ struct Settings: View { let node = nodes.first(where: { $0.num == newValue }) let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil { + if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil {// && node?.metadata == nil { let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) if adminMessageId > 0 { Logger.mesh.info("Sent node metadata request from node details") From 5852aa31f5142133597d6885febd81af80888607 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 19 Aug 2024 15:06:53 -0700 Subject: [PATCH 135/333] Admin drop down updates to redice the number of nodes. --- Localizable.xcstrings | 10 ++++++++-- Meshtastic/Views/Settings/Settings.swift | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b9858fe8..c518a26f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -17261,7 +17261,10 @@ "Remote administration for: %@" : { }, - "Remote: %@" : { + "Remote Legacy Admin: %@" : { + + }, + "Remote PKI Admin: %@" : { }, "Remove" : { @@ -17331,7 +17334,10 @@ } } }, - "Request Admin: %@" : { + "Request Legacy Admin: %@" : { + + }, + "Request PKI Admin: %@" : { }, "Requires that there be an accelerometer on your device." : { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 4bf75555..814f3ab9 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -364,21 +364,28 @@ struct Settings: View { .tag(Int(node.num)) } else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { /// Nodes using the new PKI system Label { - Text("Remote: \(node.user?.longName ?? "unknown".localized)") + Text("Remote PKI Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { Image(systemName: "av.remote") } .tag(Int(node.num)) - } else if node.metadata != nil { /// Nodes using the old admin system + } else if !UserDefaults.enableAdministration && node.metadata != nil { /// Nodes using the old admin system Label { - Text("Remote: \(node.user?.longName ?? "unknown".localized)") + Text("Remote Legacy Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { Image(systemName: "av.remote") } .tag(Int(node.num)) - } else { + } else if UserDefaults.enableAdministration && node.user?.pkiEncrypted ?? false { Label { - Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") + Text("Request PKI Admin: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "rectangle.and.hand.point.up.left") + } + .tag(Int(node.num)) + } else if !UserDefaults.enableAdministration { + Label { + Text("Request Legacy Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { Image(systemName: "rectangle.and.hand.point.up.left") } From af508451f4996f9925e19a2184f65e2237f70150 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 19 Aug 2024 16:05:21 -0700 Subject: [PATCH 136/333] update protos --- .../Sources/meshtastic/admin.pb.swift | 363 ++++++- .../Sources/meshtastic/apponly.pb.swift | 6 +- .../Sources/meshtastic/atak.pb.swift | 64 +- .../meshtastic/cannedmessages.pb.swift | 6 +- .../Sources/meshtastic/channel.pb.swift | 37 +- .../Sources/meshtastic/clientonly.pb.swift | 6 +- .../Sources/meshtastic/config.pb.swift | 593 ++++++++--- .../meshtastic/connection_status.pb.swift | 21 +- .../Sources/meshtastic/deviceonly.pb.swift | 153 ++- .../Sources/meshtastic/localonly.pb.swift | 28 +- .../Sources/meshtastic/mesh.pb.swift | 910 ++++++++++++++--- .../Sources/meshtastic/module_config.pb.swift | 265 +++-- .../Sources/meshtastic/mqtt.pb.swift | 9 +- .../Sources/meshtastic/paxcount.pb.swift | 6 +- .../Sources/meshtastic/portnums.pb.swift | 16 +- .../Sources/meshtastic/powermon.pb.swift | 115 ++- .../meshtastic/remote_hardware.pb.swift | 35 +- .../Sources/meshtastic/rtttl.pb.swift | 6 +- .../Sources/meshtastic/storeforward.pb.swift | 93 +- .../Sources/meshtastic/telemetry.pb.swift | 958 +++++++++++++----- .../Sources/meshtastic/xmodem.pb.swift | 39 +- protobufs | 2 +- 22 files changed, 2886 insertions(+), 845 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 37528079..0f1c3586 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -24,11 +24,17 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage: Sendable { +public struct AdminMessage { // 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. + /// + /// The node generates this key and sends it with any get_x_response packets. + /// The client MUST include the same key with any set_x commands. Key expires after 300 seconds. + /// Prevents replay attacks for admin messages. + public var sessionPasskey: Data = Data() + /// /// TODO: REPLACE public var payloadVariant: AdminMessage.OneOf_PayloadVariant? = nil @@ -369,6 +375,17 @@ public struct AdminMessage: Sendable { set {payloadVariant = .removeFixedPosition(newValue)} } + /// + /// Set time only on the node + /// Convenience method to set the time on the node (as Net quality) without any other position data + public var setTimeOnly: UInt32 { + get { + if case .setTimeOnly(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .setTimeOnly(newValue)} + } + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) @@ -390,6 +407,16 @@ public struct AdminMessage: Sendable { set {payloadVariant = .commitEditSettings(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 { + get { + if case .factoryResetDevice(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .factoryResetDevice(newValue)} + } + /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. @@ -433,13 +460,13 @@ public struct AdminMessage: Sendable { } /// - /// Tell the node to factory reset, all device settings will be returned to factory defaults. - public var factoryReset: Int32 { + /// Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. + public var factoryResetConfig: Int32 { get { - if case .factoryReset(let v)? = payloadVariant {return v} + if case .factoryResetConfig(let v)? = payloadVariant {return v} return 0 } - set {payloadVariant = .factoryReset(newValue)} + set {payloadVariant = .factoryResetConfig(newValue)} } /// @@ -456,7 +483,7 @@ public struct AdminMessage: Sendable { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -563,6 +590,10 @@ public struct AdminMessage: Sendable { /// Clear fixed position coordinates and then set position.fixed_position = false case removeFixedPosition(Bool) /// + /// Set time only on the node + /// Convenience method to set the time on the node (as Net quality) without any other position data + case setTimeOnly(UInt32) + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) case beginEditSettings(Bool) @@ -570,6 +601,9 @@ public struct AdminMessage: Sendable { /// Commits an open transaction for any edits made to config, module config, owner, and channel settings case commitEditSettings(Bool) /// + /// 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) + /// /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. case rebootOtaSeconds(Int32) @@ -584,17 +618,199 @@ public struct AdminMessage: Sendable { /// Tell the node to shutdown in this many seconds (or <0 to cancel shutdown) case shutdownSeconds(Int32) /// - /// Tell the node to factory reset, all device settings will be returned to factory defaults. - case factoryReset(Int32) + /// Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. + case factoryResetConfig(Int32) /// /// Tell the node to reset the nodedb. case nodedbReset(Int32) + #if !swift(>=4.1) + public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.getChannelRequest, .getChannelRequest): return { + guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getChannelResponse, .getChannelResponse): return { + guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getOwnerRequest, .getOwnerRequest): return { + guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getOwnerResponse, .getOwnerResponse): return { + guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getConfigRequest, .getConfigRequest): return { + guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getConfigResponse, .getConfigResponse): return { + guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getModuleConfigRequest, .getModuleConfigRequest): return { + guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getModuleConfigResponse, .getModuleConfigResponse): return { + guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { + guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { + guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { + guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { + guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getRingtoneRequest, .getRingtoneRequest): return { + guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getRingtoneResponse, .getRingtoneResponse): return { + guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { + guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { + guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setHamMode, .setHamMode): return { + guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { + guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { + guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.enterDfuModeRequest, .enterDfuModeRequest): return { + guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.deleteFileRequest, .deleteFileRequest): return { + guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setScale, .setScale): return { + guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setOwner, .setOwner): return { + guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setChannel, .setChannel): return { + guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setConfig, .setConfig): return { + guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setModuleConfig, .setModuleConfig): return { + guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { + guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setRingtoneMessage, .setRingtoneMessage): return { + guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeByNodenum, .removeByNodenum): return { + guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFavoriteNode, .setFavoriteNode): return { + guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFavoriteNode, .removeFavoriteNode): return { + guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setFixedPosition, .setFixedPosition): return { + guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeFixedPosition, .removeFixedPosition): return { + guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.setTimeOnly, .setTimeOnly): return { + guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.beginEditSettings, .beginEditSettings): return { + guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.commitEditSettings, .commitEditSettings): return { + guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.factoryResetDevice, .factoryResetDevice): return { + guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebootOtaSeconds, .rebootOtaSeconds): return { + guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.exitSimulator, .exitSimulator): return { + guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebootSeconds, .rebootSeconds): return { + guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.shutdownSeconds, .shutdownSeconds): return { + guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.factoryResetConfig, .factoryResetConfig): return { + guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.nodedbReset, .nodedbReset): return { + guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ConfigType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -624,6 +840,10 @@ public struct AdminMessage: Sendable { /// /// TODO: REPLACE case bluetoothConfig // = 6 + + /// + /// TODO: REPLACE + case securityConfig // = 7 case UNRECOGNIZED(Int) public init() { @@ -639,6 +859,7 @@ public struct AdminMessage: Sendable { case 4: self = .displayConfig case 5: self = .loraConfig case 6: self = .bluetoothConfig + case 7: self = .securityConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -652,26 +873,16 @@ public struct AdminMessage: Sendable { case .displayConfig: return 4 case .loraConfig: return 5 case .bluetoothConfig: return 6 + case .securityConfig: return 7 case .UNRECOGNIZED(let i): return i } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - ] - } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ModuleConfigType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -769,31 +980,51 @@ public struct AdminMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] - } public init() {} } +#if swift(>=4.2) + +extension AdminMessage.ConfigType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + .securityConfig, + ] +} + +extension AdminMessage.ModuleConfigType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] +} + +#endif // swift(>=4.2) + /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters: Sendable { +public struct HamParameters { // 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. @@ -823,7 +1054,7 @@ public struct HamParameters: Sendable { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse: Sendable { +public struct NodeRemoteHardwarePinsResponse { // 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. @@ -837,6 +1068,15 @@ public struct NodeRemoteHardwarePinsResponse: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension AdminMessage: @unchecked Sendable {} +extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} +extension AdminMessage.ConfigType: @unchecked Sendable {} +extension AdminMessage.ModuleConfigType: @unchecked Sendable {} +extension HamParameters: @unchecked Sendable {} +extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -844,6 +1084,7 @@ fileprivate let _protobuf_package = "meshtastic" extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AdminMessage" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 101: .standard(proto: "session_passkey"), 1: .standard(proto: "get_channel_request"), 2: .standard(proto: "get_channel_response"), 3: .standard(proto: "get_owner_request"), @@ -877,13 +1118,15 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 40: .standard(proto: "remove_favorite_node"), 41: .standard(proto: "set_fixed_position"), 42: .standard(proto: "remove_fixed_position"), + 43: .standard(proto: "set_time_only"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), + 94: .standard(proto: "factory_reset_device"), 95: .standard(proto: "reboot_ota_seconds"), 96: .standard(proto: "exit_simulator"), 97: .standard(proto: "reboot_seconds"), 98: .standard(proto: "shutdown_seconds"), - 99: .standard(proto: "factory_reset"), + 99: .standard(proto: "factory_reset_config"), 100: .standard(proto: "nodedb_reset"), ] @@ -1222,6 +1465,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .removeFixedPosition(v) } }() + case 43: try { + var v: UInt32? + try decoder.decodeSingularFixed32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .setTimeOnly(v) + } + }() case 64: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1238,6 +1489,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .commitEditSettings(v) } }() + case 94: try { + var v: Int32? + try decoder.decodeSingularInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .factoryResetDevice(v) + } + }() case 95: try { var v: Int32? try decoder.decodeSingularInt32Field(value: &v) @@ -1275,7 +1534,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try decoder.decodeSingularInt32Field(value: &v) if let v = v { if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} - self.payloadVariant = .factoryReset(v) + self.payloadVariant = .factoryResetConfig(v) } }() case 100: try { @@ -1286,6 +1545,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .nodedbReset(v) } }() + case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }() default: break } } @@ -1429,6 +1689,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .removeFixedPosition(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 42) }() + case .setTimeOnly?: try { + guard case .setTimeOnly(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularFixed32Field(value: v, fieldNumber: 43) + }() case .beginEditSettings?: try { guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 64) @@ -1437,6 +1701,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .commitEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 65) }() + case .factoryResetDevice?: try { + guard case .factoryResetDevice(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularInt32Field(value: v, fieldNumber: 94) + }() case .rebootOtaSeconds?: try { guard case .rebootOtaSeconds(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 95) @@ -1453,8 +1721,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .shutdownSeconds(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 98) }() - case .factoryReset?: try { - guard case .factoryReset(let v)? = self.payloadVariant else { preconditionFailure() } + case .factoryResetConfig?: try { + guard case .factoryResetConfig(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 99) }() case .nodedbReset?: try { @@ -1463,10 +1731,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat }() case nil: break } + if !self.sessionPasskey.isEmpty { + try visitor.visitSingularBytesField(value: self.sessionPasskey, fieldNumber: 101) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: AdminMessage, rhs: AdminMessage) -> Bool { + if lhs.sessionPasskey != rhs.sessionPasskey {return false} if lhs.payloadVariant != rhs.payloadVariant {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -1482,6 +1754,7 @@ extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { 4: .same(proto: "DISPLAY_CONFIG"), 5: .same(proto: "LORA_CONFIG"), 6: .same(proto: "BLUETOOTH_CONFIG"), + 7: .same(proto: "SECURITY_CONFIG"), ] } @@ -1534,7 +1807,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency.bitPattern != 0 { + if self.frequency != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 18e66d8e..0457077c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet: Sendable { +public struct ChannelSet { // 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. @@ -53,6 +53,10 @@ public struct ChannelSet: Sendable { fileprivate var _loraConfig: Config.LoRaConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension ChannelSet: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 1dd12469..4406deb3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum Team: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -130,6 +130,11 @@ public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -148,12 +153,13 @@ public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { .darkGreen, .brown, ] - } +#endif // swift(>=4.2) + /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum MemberRole: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -227,6 +233,11 @@ public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -239,12 +250,13 @@ public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { .rto, .k9, ] - } +#endif // swift(>=4.2) + /// /// Packets for the official ATAK Plugin -public struct TAKPacket: Sendable { +public struct TAKPacket { // 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. @@ -314,7 +326,7 @@ public struct TAKPacket: Sendable { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TAK position report case pli(PLI) @@ -322,6 +334,24 @@ public struct TAKPacket: Sendable { /// ATAK GeoChat message case chat(GeoChat) + #if !swift(>=4.1) + public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.pli, .pli): return { + guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.chat, .chat): return { + guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -333,7 +363,7 @@ public struct TAKPacket: Sendable { /// /// ATAK GeoChat message -public struct GeoChat: Sendable { +public struct GeoChat { // 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. @@ -375,7 +405,7 @@ public struct GeoChat: Sendable { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group: Sendable { +public struct Group { // 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. @@ -397,7 +427,7 @@ public struct Group: Sendable { /// /// ATAK EUD Status /// -public struct Status: Sendable { +public struct Status { // 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. @@ -414,7 +444,7 @@ public struct Status: Sendable { /// /// ATAK Contact /// -public struct Contact: Sendable { +public struct Contact { // 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. @@ -434,7 +464,7 @@ public struct Contact: Sendable { /// /// Position Location Information from ATAK -public struct PLI: Sendable { +public struct PLI { // 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. @@ -466,6 +496,18 @@ public struct PLI: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Team: @unchecked Sendable {} +extension MemberRole: @unchecked Sendable {} +extension TAKPacket: @unchecked Sendable {} +extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} +extension GeoChat: @unchecked Sendable {} +extension Group: @unchecked Sendable {} +extension Status: @unchecked Sendable {} +extension Contact: @unchecked Sendable {} +extension PLI: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index a43393e1..1b8c84de 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig: Sendable { +public struct CannedMessageModuleConfig { // 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. @@ -36,6 +36,10 @@ public struct CannedMessageModuleConfig: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension CannedMessageModuleConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index a8c96595..5b9c7e49 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -36,15 +36,13 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings: @unchecked Sendable { +public struct ChannelSettings { // 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. /// /// Deprecated in favor of LoraConfig.channel_num - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -113,7 +111,7 @@ public struct ChannelSettings: @unchecked Sendable { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings: Sendable { +public struct ModuleSettings { // 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. @@ -134,7 +132,7 @@ public struct ModuleSettings: Sendable { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel: Sendable { +public struct Channel { // 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. @@ -172,7 +170,7 @@ public struct Channel: Sendable { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Role: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -211,13 +209,6 @@ public struct Channel: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] - } public init() {} @@ -225,6 +216,26 @@ public struct Channel: Sendable { fileprivate var _settings: ChannelSettings? = nil } +#if swift(>=4.2) + +extension Channel.Role: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension ChannelSettings: @unchecked Sendable {} +extension ModuleSettings: @unchecked Sendable {} +extension Channel: @unchecked Sendable {} +extension Channel.Role: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index 89370cc5..c3d93bf7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile: Sendable { +public struct DeviceProfile { // 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. @@ -94,6 +94,10 @@ public struct DeviceProfile: Sendable { fileprivate var _moduleConfig: LocalModuleConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension DeviceProfile: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index aeaf9054..4b953470 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config: Sendable { +public struct Config { // 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. @@ -85,11 +85,19 @@ public struct Config: Sendable { set {payloadVariant = .bluetooth(newValue)} } + public var security: Config.SecurityConfig { + get { + if case .security(let v)? = payloadVariant {return v} + return Config.SecurityConfig() + } + set {payloadVariant = .security(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -97,12 +105,55 @@ public struct Config: Sendable { case display(Config.DisplayConfig) case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) + case security(Config.SecurityConfig) + #if !swift(>=4.1) + public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.device, .device): return { + guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.position, .position): return { + guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.power, .power): return { + guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.network, .network): return { + guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.display, .display): return { + guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.lora, .lora): return { + guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.bluetooth, .bluetooth): return { + guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.security, .security): return { + guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// Configuration - public struct DeviceConfig: Sendable { + public struct DeviceConfig { // 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. @@ -113,11 +164,13 @@ public struct Config: Sendable { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI + /// Moved to SecurityConfig public var serialEnabled: Bool = false /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). /// Set this to true to leave the debug log outputting even when API is active. + /// Moved to SecurityConfig public var debugLogEnabled: Bool = false /// @@ -146,6 +199,7 @@ public struct Config: Sendable { /// /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface + /// Moved to SecurityConfig public var isManaged: Bool = false /// @@ -164,7 +218,7 @@ public struct Config: Sendable { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Role: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -182,8 +236,6 @@ public struct Config: Sendable { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 - - /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -274,26 +326,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - ] - } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RebroadcastMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -341,14 +378,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - ] - } public init() {} @@ -356,7 +385,7 @@ public struct Config: Sendable { /// /// Position Config - public struct PositionConfig: Sendable { + public struct PositionConfig { // 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. @@ -378,8 +407,6 @@ public struct Config: Sendable { /// /// Is GPS enabled for this node? - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -390,8 +417,6 @@ public struct Config: Sendable { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -432,7 +457,7 @@ public struct Config: Sendable { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum PositionFlags: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -522,24 +547,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] - } - public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum GpsMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -577,13 +587,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] - } public init() {} @@ -592,7 +595,7 @@ public struct Config: Sendable { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig: Sendable { + public struct PowerConfig { // 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. @@ -652,7 +655,7 @@ public struct Config: Sendable { /// /// Network Config - public struct NetworkConfig: Sendable { + public struct NetworkConfig { // 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. @@ -699,7 +702,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum AddressMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -731,15 +734,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] - } - public struct IpV4Config: Sendable { + public struct IpV4Config { // 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. @@ -772,7 +769,7 @@ public struct Config: Sendable { /// /// Display Config - public struct DisplayConfig: Sendable { + public struct DisplayConfig { // 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. @@ -828,7 +825,7 @@ public struct Config: Sendable { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -891,21 +888,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] - } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum DisplayUnits: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -937,17 +924,11 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] - } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum OledType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -991,17 +972,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - ] - } - public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum DisplayMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1045,17 +1018,9 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] - } - public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum CompassOrientation: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1123,18 +1088,6 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] - } public init() {} @@ -1142,7 +1095,7 @@ public struct Config: Sendable { /// /// Lora Config - public struct LoRaConfig: @unchecked Sendable { + public struct LoRaConfig { // 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. @@ -1299,7 +1252,7 @@ public struct Config: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RegionCode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1433,35 +1386,12 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - ] - } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum ModemPreset: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1474,6 +1404,7 @@ public struct Config: Sendable { /// /// Very Long Range - Slow + /// Deprecated in 2.5: Works only with txco and is unusably slow case veryLongSlow // = 2 /// @@ -1495,6 +1426,12 @@ public struct Config: Sendable { /// /// Long Range - Moderately Fast case longModerate // = 7 + + /// + /// Short Range - Turbo + /// This is the fastest preset and the only one with 500kHz bandwidth. + /// It is not legal to use in all regions due to this wider bandwidth. + case shortTurbo // = 8 case UNRECOGNIZED(Int) public init() { @@ -1511,6 +1448,7 @@ public struct Config: Sendable { case 5: self = .shortSlow case 6: self = .shortFast case 7: self = .longModerate + case 8: self = .shortTurbo default: self = .UNRECOGNIZED(rawValue) } } @@ -1525,22 +1463,11 @@ public struct Config: Sendable { case .shortSlow: return 5 case .shortFast: return 6 case .longModerate: return 7 + case .shortTurbo: return 8 case .UNRECOGNIZED(let i): return i } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - ] - } public init() {} @@ -1548,7 +1475,7 @@ public struct Config: Sendable { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig: Sendable { + public struct BluetoothConfig { // 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. @@ -1567,11 +1494,12 @@ public struct Config: Sendable { /// /// Enables device (serial style logs) over Bluetooth + /// Moved to SecurityConfig public var deviceLoggingEnabled: Bool = false public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum PairingMode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1609,21 +1537,255 @@ public struct Config: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] - } public init() {} } + public struct SecurityConfig { + // 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. + + /// + /// The public key of the user's device. + /// Sent out to other nodes on the mesh to allow them to compute a shared secret key. + public var publicKey: Data = Data() + + /// + /// The private key of the device. + /// Used to create a shared key with a remote device. + public var privateKey: Data = Data() + + /// + /// The public key authorized to send admin messages to this node. + public var adminKey: Data = Data() + + /// + /// If true, device is considered to be "managed" by a mesh administrator via admin messages + /// Device is managed by a mesh administrator. + public var isManaged: Bool = false + + /// + /// Serial Console over the Stream API." + public var serialEnabled: Bool = false + + /// + /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). + /// Output live debug logging over serial. + public var debugLogApiEnabled: Bool = false + + /// + /// Enables device (serial style logs) over Bluetooth + public var bluetoothLoggingEnabled: Bool = false + + /// + /// Allow incoming device control over the insecure legacy admin channel. + public var adminChannelEnabled: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } +#if swift(>=4.2) + +extension Config.DeviceConfig.Role: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + ] +} + +extension Config.DeviceConfig.RebroadcastMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + ] +} + +extension Config.PositionConfig.PositionFlags: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] +} + +extension Config.PositionConfig.GpsMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] +} + +extension Config.NetworkConfig.AddressMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] +} + +extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] +} + +extension Config.DisplayConfig.DisplayUnits: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] +} + +extension Config.DisplayConfig.OledType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + ] +} + +extension Config.DisplayConfig.DisplayMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] +} + +extension Config.DisplayConfig.CompassOrientation: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] +} + +extension Config.LoRaConfig.RegionCode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + ] +} + +extension Config.LoRaConfig.ModemPreset: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + .shortTurbo, + ] +} + +extension Config.BluetoothConfig.PairingMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension Config: @unchecked Sendable {} +extension Config.OneOf_PayloadVariant: @unchecked Sendable {} +extension Config.DeviceConfig: @unchecked Sendable {} +extension Config.DeviceConfig.Role: @unchecked Sendable {} +extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} +extension Config.PositionConfig: @unchecked Sendable {} +extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} +extension Config.PositionConfig.GpsMode: @unchecked Sendable {} +extension Config.PowerConfig: @unchecked Sendable {} +extension Config.NetworkConfig: @unchecked Sendable {} +extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} +extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} +extension Config.DisplayConfig: @unchecked Sendable {} +extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} +extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} +extension Config.DisplayConfig.OledType: @unchecked Sendable {} +extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} +extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} +extension Config.LoRaConfig: @unchecked Sendable {} +extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} +extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} +extension Config.BluetoothConfig: @unchecked Sendable {} +extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} +extension Config.SecurityConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1638,6 +1800,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 5: .same(proto: "display"), 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), + 8: .same(proto: "security"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1737,6 +1900,19 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas self.payloadVariant = .bluetooth(v) } }() + case 8: try { + var v: Config.SecurityConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .security(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .security(v) + } + }() default: break } } @@ -1776,6 +1952,10 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas guard case .bluetooth(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 7) }() + case .security?: try { + guard case .security(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2080,7 +2260,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride.bitPattern != 0 { + if self.adcMultiplierOverride != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2524,7 +2704,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset.bitPattern != 0 { + if _storage._frequencyOffset != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2548,7 +2728,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency.bitPattern != 0 { + if _storage._overrideFrequency != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { @@ -2629,6 +2809,7 @@ extension Config.LoRaConfig.ModemPreset: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "SHORT_SLOW"), 6: .same(proto: "SHORT_FAST"), 7: .same(proto: "LONG_MODERATE"), + 8: .same(proto: "SHORT_TURBO"), ] } @@ -2689,3 +2870,77 @@ extension Config.BluetoothConfig.PairingMode: SwiftProtobuf._ProtoNameProviding 2: .same(proto: "NO_PIN"), ] } + +extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Config.protoMessageName + ".SecurityConfig" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "public_key"), + 2: .standard(proto: "private_key"), + 3: .standard(proto: "admin_key"), + 4: .standard(proto: "is_managed"), + 5: .standard(proto: "serial_enabled"), + 6: .standard(proto: "debug_log_api_enabled"), + 7: .standard(proto: "bluetooth_logging_enabled"), + 8: .standard(proto: "admin_channel_enabled"), + ] + + 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.decodeSingularBytesField(value: &self.publicKey) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.privateKey) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.adminKey) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() + case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogApiEnabled) }() + case 7: try { try decoder.decodeSingularBoolField(value: &self.bluetoothLoggingEnabled) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.adminChannelEnabled) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.publicKey.isEmpty { + try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 1) + } + if !self.privateKey.isEmpty { + try visitor.visitSingularBytesField(value: self.privateKey, fieldNumber: 2) + } + if !self.adminKey.isEmpty { + try visitor.visitSingularBytesField(value: self.adminKey, fieldNumber: 3) + } + if self.isManaged != false { + try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 4) + } + if self.serialEnabled != false { + try visitor.visitSingularBoolField(value: self.serialEnabled, fieldNumber: 5) + } + if self.debugLogApiEnabled != false { + try visitor.visitSingularBoolField(value: self.debugLogApiEnabled, fieldNumber: 6) + } + if self.bluetoothLoggingEnabled != false { + try visitor.visitSingularBoolField(value: self.bluetoothLoggingEnabled, fieldNumber: 7) + } + if self.adminChannelEnabled != false { + try visitor.visitSingularBoolField(value: self.adminChannelEnabled, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Config.SecurityConfig, rhs: Config.SecurityConfig) -> Bool { + if lhs.publicKey != rhs.publicKey {return false} + if lhs.privateKey != rhs.privateKey {return false} + if lhs.adminKey != rhs.adminKey {return false} + if lhs.isManaged != rhs.isManaged {return false} + if lhs.serialEnabled != rhs.serialEnabled {return false} + if lhs.debugLogApiEnabled != rhs.debugLogApiEnabled {return false} + if lhs.bluetoothLoggingEnabled != rhs.bluetoothLoggingEnabled {return false} + if lhs.adminChannelEnabled != rhs.adminChannelEnabled {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a4569714..a2ec180e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus: Sendable { +public struct DeviceConnectionStatus { // 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus: Sendable { /// /// WiFi connection status -public struct WifiConnectionStatus: Sendable { +public struct WifiConnectionStatus { // 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus: Sendable { /// /// Ethernet connection status -public struct EthernetConnectionStatus: Sendable { +public struct EthernetConnectionStatus { // 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus: Sendable { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus: Sendable { +public struct NetworkConnectionStatus { // 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus: Sendable { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus: Sendable { +public struct BluetoothConnectionStatus { // 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus: Sendable { /// /// Serial connection status -public struct SerialConnectionStatus: Sendable { +public struct SerialConnectionStatus { // 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. @@ -209,6 +209,15 @@ public struct SerialConnectionStatus: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension DeviceConnectionStatus: @unchecked Sendable {} +extension WifiConnectionStatus: @unchecked Sendable {} +extension EthernetConnectionStatus: @unchecked Sendable {} +extension NetworkConnectionStatus: @unchecked Sendable {} +extension BluetoothConnectionStatus: @unchecked Sendable {} +extension SerialConnectionStatus: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 834f9636..43506399 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum ScreenFonts: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -60,18 +60,24 @@ public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, ] - } +#endif // swift(>=4.2) + /// /// Position with static location information only for NodeDBLite -public struct PositionLite: Sendable { +public struct PositionLite { // 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. @@ -106,7 +112,52 @@ public struct PositionLite: Sendable { public init() {} } -public struct NodeInfoLite: @unchecked Sendable { +public struct UserLite { + // 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. + + /// + /// This is the addr of the radio. + public var macaddr: Data = Data() + + /// + /// A full name for this user, i.e. "Kevin Hester" + public var longName: String = String() + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + public var shortName: String = String() + + /// + /// TBEAM, HELTEC, etc... + /// Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. + /// Apps will still need the string here for older builds + /// (so OTA update can find the right image), but if the enum is available it will be used instead. + public var hwModel: HardwareModel = .unset + + /// + /// In some regions Ham radio operators have different bandwidth limitations than others. + /// If this user is a licensed operator, set this flag. + /// Also, "long_name" should be their licence number. + public var isLicensed: Bool = false + + /// + /// Indicates that the user's role in the mesh + public var role: Config.DeviceConfig.Role = .client + + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + public var publicKey: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct NodeInfoLite { // 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. @@ -120,8 +171,8 @@ public struct NodeInfoLite: @unchecked Sendable { /// /// The user info for this node - public var user: User { - get {return _storage._user ?? User()} + public var user: UserLite { + get {return _storage._user ?? UserLite()} set {_uniqueStorage()._user = newValue} } /// Returns true if `user` has been explicitly set. @@ -209,7 +260,7 @@ public struct NodeInfoLite: @unchecked Sendable { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState: @unchecked Sendable { +public struct DeviceState { // 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. @@ -269,8 +320,6 @@ public struct DeviceState: @unchecked Sendable { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -319,7 +368,7 @@ public struct DeviceState: @unchecked Sendable { /// /// The on-disk saved channels -public struct ChannelFile: Sendable { +public struct ChannelFile { // 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. @@ -342,7 +391,7 @@ public struct ChannelFile: Sendable { /// /// This can be used for customizing the firmware distribution. If populated, /// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore: @unchecked Sendable { +public struct OEMStore { // 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. @@ -401,6 +450,16 @@ public struct OEMStore: @unchecked Sendable { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension ScreenFonts: @unchecked Sendable {} +extension PositionLite: @unchecked Sendable {} +extension UserLite: @unchecked Sendable {} +extension NodeInfoLite: @unchecked Sendable {} +extension DeviceState: @unchecked Sendable {} +extension ChannelFile: @unchecked Sendable {} +extension OEMStore: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -469,6 +528,74 @@ extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } } +extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".UserLite" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "macaddr"), + 2: .standard(proto: "long_name"), + 3: .standard(proto: "short_name"), + 4: .standard(proto: "hw_model"), + 5: .standard(proto: "is_licensed"), + 6: .same(proto: "role"), + 7: .standard(proto: "public_key"), + ] + + 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.decodeSingularBytesField(value: &self.macaddr) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.longName) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.shortName) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 7: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.macaddr.isEmpty { + try visitor.visitSingularBytesField(value: self.macaddr, fieldNumber: 1) + } + if !self.longName.isEmpty { + try visitor.visitSingularStringField(value: self.longName, fieldNumber: 2) + } + if !self.shortName.isEmpty { + try visitor.visitSingularStringField(value: self.shortName, fieldNumber: 3) + } + if self.hwModel != .unset { + try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 4) + } + if self.isLicensed != false { + try visitor.visitSingularBoolField(value: self.isLicensed, fieldNumber: 5) + } + if self.role != .client { + try visitor.visitSingularEnumField(value: self.role, fieldNumber: 6) + } + if !self.publicKey.isEmpty { + try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: UserLite, rhs: UserLite) -> Bool { + if lhs.macaddr != rhs.macaddr {return false} + if lhs.longName != rhs.longName {return false} + if lhs.shortName != rhs.shortName {return false} + if lhs.hwModel != rhs.hwModel {return false} + if lhs.isLicensed != rhs.isLicensed {return false} + if lhs.role != rhs.role {return false} + if lhs.publicKey != rhs.publicKey {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".NodeInfoLite" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -486,7 +613,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat fileprivate class _StorageClass { var _num: UInt32 = 0 - var _user: User? = nil + var _user: UserLite? = nil var _position: PositionLite? = nil var _snr: Float = 0 var _lastHeard: UInt32 = 0 @@ -568,7 +695,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr.bitPattern != 0 { + if _storage._snr != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 17dd5baa..0af27466 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig: @unchecked Sendable { +public struct LocalConfig { // 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. @@ -111,6 +111,17 @@ public struct LocalConfig: @unchecked Sendable { set {_uniqueStorage()._version = newValue} } + /// + /// The part of the config that is specific to Security settings + public var security: Config.SecurityConfig { + get {return _storage._security ?? Config.SecurityConfig()} + set {_uniqueStorage()._security = newValue} + } + /// Returns true if `security` has been explicitly set. + public var hasSecurity: Bool {return _storage._security != nil} + /// Clears the value of `security`. Subsequent reads from it will return its default value. + public mutating func clearSecurity() {_uniqueStorage()._security = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -118,7 +129,7 @@ public struct LocalConfig: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig: @unchecked Sendable { +public struct LocalModuleConfig { // 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. @@ -282,6 +293,11 @@ public struct LocalModuleConfig: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=5.5) && canImport(_Concurrency) +extension LocalConfig: @unchecked Sendable {} +extension LocalModuleConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -297,6 +313,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), 8: .same(proto: "version"), + 9: .same(proto: "security"), ] fileprivate class _StorageClass { @@ -308,6 +325,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _lora: Config.LoRaConfig? = nil var _bluetooth: Config.BluetoothConfig? = nil var _version: UInt32 = 0 + var _security: Config.SecurityConfig? = nil #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -330,6 +348,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati _lora = source._lora _bluetooth = source._bluetooth _version = source._version + _security = source._security } } @@ -356,6 +375,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati case 6: try { try decoder.decodeSingularMessageField(value: &_storage._lora) }() case 7: try { try decoder.decodeSingularMessageField(value: &_storage._bluetooth) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }() + case 9: try { try decoder.decodeSingularMessageField(value: &_storage._security) }() default: break } } @@ -392,6 +412,9 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if _storage._version != 0 { try visitor.visitSingularUInt32Field(value: _storage._version, fieldNumber: 8) } + try { if let v = _storage._security { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -409,6 +432,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if _storage._lora != rhs_storage._lora {return false} if _storage._bluetooth != rhs_storage._bluetooth {return false} if _storage._version != rhs_storage._version {return false} + if _storage._security != rhs_storage._security {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 996e8268..ba12908d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -25,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum HardwareModel: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -329,6 +329,23 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors. case trackerT1000E // = 71 + /// + /// RAK3172 STM32WLE5 Module (https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172) + case rak3172 // = 72 + + /// + /// Seeed Studio Wio-E5 (either mini or Dev kit) using STM32WL chip. + case wioE5 // = 73 + + /// + /// RadioMaster 900 Bandit, https://www.radiomasterrc.com/products/bandit-expresslrs-rf-module + /// SSD1306 OLED and No GPS + case radiomaster900Bandit // = 74 + + /// + /// Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. + case me25Ls014Y10Td // = 75 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -413,6 +430,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 69: self = .heltecMeshNodeT114 case 70: self = .sensecapIndicator case 71: self = .trackerT1000E + case 72: self = .rak3172 + case 73: self = .wioE5 + case 74: self = .radiomaster900Bandit + case 75: self = .me25Ls014Y10Td case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -491,11 +512,20 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .heltecMeshNodeT114: return 69 case .sensecapIndicator: return 70 case .trackerT1000E: return 71 + case .rak3172: return 72 + case .wioE5: return 73 + case .radiomaster900Bandit: return 74 + case .me25Ls014Y10Td: return 75 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } } +} + +#if swift(>=4.2) + +extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -569,14 +599,19 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .heltecMeshNodeT114, .sensecapIndicator, .trackerT1000E, + .rak3172, + .wioE5, + .radiomaster900Bandit, + .me25Ls014Y10Td, .privateHw, ] - } +#endif // swift(>=4.2) + /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum Constants: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -611,20 +646,26 @@ public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] - } +#endif // swift(>=4.2) + /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum CriticalErrorCode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -676,6 +717,17 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { /// A (likely software but possibly hardware) failure was detected while trying to send packets. /// If this occurs on your board, please post in the forum so that we can ask you to collect some information to allow fixing this bug case radioSpiBug // = 11 + + /// + /// Corruption was detected on the flash filesystem but we were able to repair things. + /// If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. + case flashCorruptionRecoverable // = 12 + + /// + /// Corruption was detected on the flash filesystem but we were unable to repair things. + /// NOTE: Your node will probably need to be reconfigured the next time it reboots (it will lose the region code etc...) + /// If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. + case flashCorruptionUnrecoverable // = 13 case UNRECOGNIZED(Int) public init() { @@ -696,6 +748,8 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { case 9: self = .brownout case 10: self = .sx1262Failure case 11: self = .radioSpiBug + case 12: self = .flashCorruptionRecoverable + case 13: self = .flashCorruptionUnrecoverable default: self = .UNRECOGNIZED(rawValue) } } @@ -714,10 +768,17 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { case .brownout: return 9 case .sx1262Failure: return 10 case .radioSpiBug: return 11 + case .flashCorruptionRecoverable: return 12 + case .flashCorruptionUnrecoverable: return 13 case .UNRECOGNIZED(let i): return i } } +} + +#if swift(>=4.2) + +extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -732,13 +793,16 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { .brownout, .sx1262Failure, .radioSpiBug, + .flashCorruptionRecoverable, + .flashCorruptionUnrecoverable, ] - } +#endif // swift(>=4.2) + /// /// a gps position -public struct Position: @unchecked Sendable { +public struct Position { // 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. @@ -747,23 +811,35 @@ public struct Position: @unchecked Sendable { /// The new preferred location encoding, multiply by 1e-7 to get degrees /// in floating point public var latitudeI: Int32 { - get {return _storage._latitudeI} + get {return _storage._latitudeI ?? 0} set {_uniqueStorage()._latitudeI = newValue} } + /// Returns true if `latitudeI` has been explicitly set. + public var hasLatitudeI: Bool {return _storage._latitudeI != nil} + /// Clears the value of `latitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLatitudeI() {_uniqueStorage()._latitudeI = nil} /// /// TODO: REPLACE public var longitudeI: Int32 { - get {return _storage._longitudeI} + get {return _storage._longitudeI ?? 0} set {_uniqueStorage()._longitudeI = newValue} } + /// Returns true if `longitudeI` has been explicitly set. + public var hasLongitudeI: Bool {return _storage._longitudeI != nil} + /// Clears the value of `longitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLongitudeI() {_uniqueStorage()._longitudeI = nil} /// /// In meters above MSL (but see issue #359) public var altitude: Int32 { - get {return _storage._altitude} + get {return _storage._altitude ?? 0} set {_uniqueStorage()._altitude = newValue} } + /// Returns true if `altitude` has been explicitly set. + public var hasAltitude: Bool {return _storage._altitude != nil} + /// Clears the value of `altitude`. Subsequent reads from it will return its default value. + public mutating func clearAltitude() {_uniqueStorage()._altitude = nil} /// /// This is usually not sent over the mesh (to save space), but it is sent @@ -806,16 +882,24 @@ public struct Position: @unchecked Sendable { /// /// HAE altitude in meters - can be used instead of MSL altitude public var altitudeHae: Int32 { - get {return _storage._altitudeHae} + get {return _storage._altitudeHae ?? 0} set {_uniqueStorage()._altitudeHae = newValue} } + /// Returns true if `altitudeHae` has been explicitly set. + public var hasAltitudeHae: Bool {return _storage._altitudeHae != nil} + /// Clears the value of `altitudeHae`. Subsequent reads from it will return its default value. + public mutating func clearAltitudeHae() {_uniqueStorage()._altitudeHae = nil} /// /// Geoidal separation in meters public var altitudeGeoidalSeparation: Int32 { - get {return _storage._altitudeGeoidalSeparation} + get {return _storage._altitudeGeoidalSeparation ?? 0} set {_uniqueStorage()._altitudeGeoidalSeparation = newValue} } + /// Returns true if `altitudeGeoidalSeparation` has been explicitly set. + public var hasAltitudeGeoidalSeparation: Bool {return _storage._altitudeGeoidalSeparation != nil} + /// Clears the value of `altitudeGeoidalSeparation`. Subsequent reads from it will return its default value. + public mutating func clearAltitudeGeoidalSeparation() {_uniqueStorage()._altitudeGeoidalSeparation = nil} /// /// Horizontal, Vertical and Position Dilution of Precision, in 1/100 units @@ -859,16 +943,24 @@ public struct Position: @unchecked Sendable { /// - "yaw" indicates a relative rotation about the vertical axis /// TODO: REMOVE/INTEGRATE public var groundSpeed: UInt32 { - get {return _storage._groundSpeed} + get {return _storage._groundSpeed ?? 0} set {_uniqueStorage()._groundSpeed = newValue} } + /// Returns true if `groundSpeed` has been explicitly set. + public var hasGroundSpeed: Bool {return _storage._groundSpeed != nil} + /// Clears the value of `groundSpeed`. Subsequent reads from it will return its default value. + public mutating func clearGroundSpeed() {_uniqueStorage()._groundSpeed = nil} /// /// TODO: REPLACE public var groundTrack: UInt32 { - get {return _storage._groundTrack} + get {return _storage._groundTrack ?? 0} set {_uniqueStorage()._groundTrack = newValue} } + /// Returns true if `groundTrack` has been explicitly set. + public var hasGroundTrack: Bool {return _storage._groundTrack != nil} + /// Clears the value of `groundTrack`. Subsequent reads from it will return its default value. + public mutating func clearGroundTrack() {_uniqueStorage()._groundTrack = nil} /// /// GPS fix quality (from NMEA GxGGA statement or similar) @@ -927,7 +1019,7 @@ public struct Position: @unchecked Sendable { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum LocSource: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -971,20 +1063,12 @@ public struct Position: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] - } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum AltSource: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1034,15 +1118,6 @@ public struct Position: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] - } public init() {} @@ -1050,6 +1125,31 @@ public struct Position: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=4.2) + +extension Position.LocSource: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] +} + +extension Position.AltSource: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] +} + +#endif // swift(>=4.2) + /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1071,7 +1171,7 @@ public struct Position: @unchecked Sendable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User: @unchecked Sendable { +public struct User { // 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. @@ -1096,8 +1196,6 @@ public struct User: @unchecked Sendable { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1117,22 +1215,39 @@ public struct User: @unchecked Sendable { /// Indicates that the user's role in the mesh public var role: Config.DeviceConfig.Role = .client + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + public var publicKey: Data = Data() + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} } /// -/// A message used in our Dynamic Source Routing protocol (RFC 4728 based) -public struct RouteDiscovery: Sendable { +/// A message used in a traceroute +public struct RouteDiscovery { // 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. /// - /// The list of nodenums this packet has visited so far + /// The list of nodenums this packet has visited so far to the destination. public var route: [UInt32] = [] + /// + /// The list of SNRs (in dB, scaled by 4) in the route towards the destination. + public var snrTowards: [Int32] = [] + + /// + /// The list of nodenums the packet has visited on the way back from the destination. + public var routeBack: [UInt32] = [] + + /// + /// The list of SNRs (in dB, scaled by 4) in the route back from the destination. + public var snrBack: [Int32] = [] + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1140,7 +1255,7 @@ public struct RouteDiscovery: Sendable { /// /// A Routing control Data packet handled by the routing module -public struct Routing: Sendable { +public struct Routing { // 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. @@ -1180,7 +1295,7 @@ public struct Routing: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable, Sendable { + public enum OneOf_Variant: Equatable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1192,12 +1307,34 @@ public struct Routing: Sendable { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) + #if !swift(>=4.1) + public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.routeRequest, .routeRequest): return { + guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.routeReply, .routeReply): return { + guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorReason, .errorReason): return { + guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Error: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1249,6 +1386,14 @@ public struct Routing: Sendable { /// The application layer service on the remote node received your request, but considered your request not authorized /// (i.e you did not send the request on the required bound channel) case notAuthorized // = 33 + + /// + /// The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all) + case pkiFailed // = 34 + + /// + /// The receiving node does not have a Public Key to decode with + case pkiUnknownPubkey // = 35 case UNRECOGNIZED(Int) public init() { @@ -1269,6 +1414,8 @@ public struct Routing: Sendable { case 9: self = .dutyCycleLimit case 32: self = .badRequest case 33: self = .notAuthorized + case 34: self = .pkiFailed + case 35: self = .pkiUnknownPubkey default: self = .UNRECOGNIZED(rawValue) } } @@ -1287,36 +1434,46 @@ public struct Routing: Sendable { case .dutyCycleLimit: return 9 case .badRequest: return 32 case .notAuthorized: return 33 + case .pkiFailed: return 34 + case .pkiUnknownPubkey: return 35 case .UNRECOGNIZED(let i): return i } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - ] - } public init() {} } +#if swift(>=4.2) + +extension Routing.Error: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + .pkiFailed, + .pkiUnknownPubkey, + ] +} + +#endif // swift(>=4.2) + /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage: @unchecked Sendable { +public struct DataMessage { // 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. @@ -1370,7 +1527,7 @@ public struct DataMessage: @unchecked Sendable { /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint: Sendable { +public struct Waypoint { // 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. @@ -1381,11 +1538,25 @@ public struct Waypoint: Sendable { /// /// latitude_i - public var latitudeI: Int32 = 0 + public var latitudeI: Int32 { + get {return _latitudeI ?? 0} + set {_latitudeI = newValue} + } + /// Returns true if `latitudeI` has been explicitly set. + public var hasLatitudeI: Bool {return self._latitudeI != nil} + /// Clears the value of `latitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLatitudeI() {self._latitudeI = nil} /// /// longitude_i - public var longitudeI: Int32 = 0 + public var longitudeI: Int32 { + get {return _longitudeI ?? 0} + set {_longitudeI = newValue} + } + /// Returns true if `longitudeI` has been explicitly set. + public var hasLongitudeI: Bool {return self._longitudeI != nil} + /// Clears the value of `longitudeI`. Subsequent reads from it will return its default value. + public mutating func clearLongitudeI() {self._longitudeI = nil} /// /// Time the waypoint is to expire (epoch) @@ -1411,11 +1582,14 @@ public struct Waypoint: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _latitudeI: Int32? = nil + fileprivate var _longitudeI: Int32? = nil } /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage: @unchecked Sendable { +public struct MqttClientProxyMessage { // 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. @@ -1456,7 +1630,7 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Bytes case data(Data) @@ -1464,6 +1638,24 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// Text case text(String) + #if !swift(>=4.1) + public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.data, .data): return { + guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -1473,7 +1665,7 @@ public struct MqttClientProxyMessage: @unchecked Sendable { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket: @unchecked Sendable { +public struct MeshPacket { // 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. @@ -1607,8 +1799,6 @@ public struct MeshPacket: @unchecked Sendable { /// /// Describe if this message is delayed - /// - /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -1629,9 +1819,23 @@ public struct MeshPacket: @unchecked Sendable { set {_uniqueStorage()._hopStart = newValue} } + /// + /// Records the public key the packet was encrypted with, if applicable. + public var publicKey: Data { + get {return _storage._publicKey} + set {_uniqueStorage()._publicKey = newValue} + } + + /// + /// Indicates whether the packet was en/decrypted using PKI + public var pkiEncrypted: Bool { + get {return _storage._pkiEncrypted} + set {_uniqueStorage()._pkiEncrypted = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1639,6 +1843,24 @@ public struct MeshPacket: @unchecked Sendable { /// TODO: REPLACE case encrypted(Data) + #if !swift(>=4.1) + public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.decoded, .decoded): return { + guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.encrypted, .encrypted): return { + guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// @@ -1660,7 +1882,7 @@ public struct MeshPacket: @unchecked Sendable { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Priority: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1725,22 +1947,11 @@ public struct MeshPacket: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .ack, - .max, - ] - } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Delayed: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1778,13 +1989,6 @@ public struct MeshPacket: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] - } public init() {} @@ -1792,6 +1996,32 @@ public struct MeshPacket: @unchecked Sendable { fileprivate var _storage = _StorageClass.defaultInstance } +#if swift(>=4.2) + +extension MeshPacket.Priority: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .ack, + .max, + ] +} + +extension MeshPacket.Delayed: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] +} + +#endif // swift(>=4.2) + /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -1809,7 +2039,7 @@ public struct MeshPacket: @unchecked Sendable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo: @unchecked Sendable { +public struct NodeInfo { // 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. @@ -1910,7 +2140,7 @@ public struct NodeInfo: @unchecked Sendable { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo: Sendable { +public struct MyNodeInfo { // 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. @@ -1941,7 +2171,7 @@ public struct MyNodeInfo: Sendable { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord: Sendable { +public struct LogRecord { // 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. @@ -1966,7 +2196,7 @@ public struct LogRecord: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Level: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -2028,23 +2258,29 @@ public struct LogRecord: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] - } public init() {} } -public struct QueueStatus: Sendable { +#if swift(>=4.2) + +extension LogRecord.Level: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] +} + +#endif // swift(>=4.2) + +public struct QueueStatus { // 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. @@ -2071,7 +2307,7 @@ public struct QueueStatus: Sendable { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio: Sendable { +public struct FromRadio { // 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. @@ -2233,11 +2469,21 @@ public struct FromRadio: Sendable { set {payloadVariant = .fileInfo(newValue)} } + /// + /// Notification message to the client + public var clientNotification: ClientNotification { + get { + if case .clientNotification(let v)? = payloadVariant {return v} + return ClientNotification() + } + set {payloadVariant = .clientNotification(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2288,15 +2534,128 @@ public struct FromRadio: Sendable { /// /// File system manifest messages case fileInfo(FileInfo) + /// + /// Notification message to the client + case clientNotification(ClientNotification) + #if !swift(>=4.1) + public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.packet, .packet): return { + guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.myInfo, .myInfo): return { + guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.nodeInfo, .nodeInfo): return { + guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.config, .config): return { + guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.logRecord, .logRecord): return { + guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.configCompleteID, .configCompleteID): return { + guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rebooted, .rebooted): return { + guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.moduleConfig, .moduleConfig): return { + guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.channel, .channel): return { + guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.queueStatus, .queueStatus): return { + guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.metadata, .metadata): return { + guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { + guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileInfo, .fileInfo): return { + guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.clientNotification, .clientNotification): return { + guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} } +/// +/// A notification message from the device to the client +/// To be used for important messages that should to be displayed to the user +/// in the form of push notifications or validation messages when saving +/// invalid configuration. +public struct ClientNotification { + // 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. + + /// + /// The id of the packet we're notifying in response to + public var replyID: UInt32 { + get {return _replyID ?? 0} + set {_replyID = newValue} + } + /// Returns true if `replyID` has been explicitly set. + public var hasReplyID: Bool {return self._replyID != nil} + /// Clears the value of `replyID`. Subsequent reads from it will return its default value. + public mutating func clearReplyID() {self._replyID = nil} + + /// + /// Seconds since 1970 - or 0 for unknown/unset + public var time: UInt32 = 0 + + /// + /// The level type of notification + public var level: LogRecord.Level = .unset + + /// + /// The message body of the notification + public var message: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _replyID: UInt32? = nil +} + /// /// Individual File info for the device -public struct FileInfo: Sendable { +public struct FileInfo { // 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. @@ -2317,7 +2676,7 @@ public struct FileInfo: Sendable { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio: Sendable { +public struct ToRadio { // 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. @@ -2397,7 +2756,7 @@ public struct ToRadio: Sendable { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -2424,6 +2783,40 @@ public struct ToRadio: Sendable { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) + #if !swift(>=4.1) + public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.packet, .packet): return { + guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.wantConfigID, .wantConfigID): return { + guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.disconnect, .disconnect): return { + guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xmodemPacket, .xmodemPacket): return { + guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { + guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -2431,7 +2824,7 @@ public struct ToRadio: Sendable { /// /// Compressed message payload -public struct Compressed: @unchecked Sendable { +public struct Compressed { // 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. @@ -2451,7 +2844,7 @@ public struct Compressed: @unchecked Sendable { /// /// Full info on edges for a single node -public struct NeighborInfo: Sendable { +public struct NeighborInfo { // 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. @@ -2479,7 +2872,7 @@ public struct NeighborInfo: Sendable { /// /// A single edge in the mesh -public struct Neighbor: Sendable { +public struct Neighbor { // 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. @@ -2509,7 +2902,7 @@ public struct Neighbor: Sendable { /// /// Device metadata response -public struct DeviceMetadata: Sendable { +public struct DeviceMetadata { // 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. @@ -2562,7 +2955,7 @@ public struct DeviceMetadata: Sendable { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat: Sendable { +public struct Heartbeat { // 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. @@ -2574,7 +2967,7 @@ public struct Heartbeat: Sendable { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin: Sendable { +public struct NodeRemoteHardwarePin { // 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. @@ -2601,7 +2994,7 @@ public struct NodeRemoteHardwarePin: Sendable { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload: @unchecked Sendable { +public struct ChunkedPayload { // 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. @@ -2629,7 +3022,7 @@ public struct ChunkedPayload: @unchecked Sendable { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks: Sendable { +public struct resend_chunks { // 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. @@ -2643,7 +3036,7 @@ public struct resend_chunks: Sendable { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse: Sendable { +public struct ChunkedPayloadResponse { // 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. @@ -2686,7 +3079,7 @@ public struct ChunkedPayloadResponse: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -2697,11 +3090,76 @@ public struct ChunkedPayloadResponse: Sendable { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) + #if !swift(>=4.1) + public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.requestTransfer, .requestTransfer): return { + guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.acceptTransfer, .acceptTransfer): return { + guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.resendChunks, .resendChunks): return { + guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension HardwareModel: @unchecked Sendable {} +extension Constants: @unchecked Sendable {} +extension CriticalErrorCode: @unchecked Sendable {} +extension Position: @unchecked Sendable {} +extension Position.LocSource: @unchecked Sendable {} +extension Position.AltSource: @unchecked Sendable {} +extension User: @unchecked Sendable {} +extension RouteDiscovery: @unchecked Sendable {} +extension Routing: @unchecked Sendable {} +extension Routing.OneOf_Variant: @unchecked Sendable {} +extension Routing.Error: @unchecked Sendable {} +extension DataMessage: @unchecked Sendable {} +extension Waypoint: @unchecked Sendable {} +extension MqttClientProxyMessage: @unchecked Sendable {} +extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} +extension MeshPacket: @unchecked Sendable {} +extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} +extension MeshPacket.Priority: @unchecked Sendable {} +extension MeshPacket.Delayed: @unchecked Sendable {} +extension NodeInfo: @unchecked Sendable {} +extension MyNodeInfo: @unchecked Sendable {} +extension LogRecord: @unchecked Sendable {} +extension LogRecord.Level: @unchecked Sendable {} +extension QueueStatus: @unchecked Sendable {} +extension FromRadio: @unchecked Sendable {} +extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension ClientNotification: @unchecked Sendable {} +extension FileInfo: @unchecked Sendable {} +extension ToRadio: @unchecked Sendable {} +extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} +extension Compressed: @unchecked Sendable {} +extension NeighborInfo: @unchecked Sendable {} +extension Neighbor: @unchecked Sendable {} +extension DeviceMetadata: @unchecked Sendable {} +extension Heartbeat: @unchecked Sendable {} +extension NodeRemoteHardwarePin: @unchecked Sendable {} +extension ChunkedPayload: @unchecked Sendable {} +extension resend_chunks: @unchecked Sendable {} +extension ChunkedPayloadResponse: @unchecked Sendable {} +extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2779,6 +3237,10 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 69: .same(proto: "HELTEC_MESH_NODE_T114"), 70: .same(proto: "SENSECAP_INDICATOR"), 71: .same(proto: "TRACKER_T1000_E"), + 72: .same(proto: "RAK3172"), + 73: .same(proto: "WIO_E5"), + 74: .same(proto: "RADIOMASTER_900_BANDIT"), + 75: .same(proto: "ME25LS01_4Y10TD"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -2804,6 +3266,8 @@ extension CriticalErrorCode: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "BROWNOUT"), 10: .same(proto: "SX1262_FAILURE"), 11: .same(proto: "RADIO_SPI_BUG"), + 12: .same(proto: "FLASH_CORRUPTION_RECOVERABLE"), + 13: .same(proto: "FLASH_CORRUPTION_UNRECOVERABLE"), ] } @@ -2836,22 +3300,22 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB ] fileprivate class _StorageClass { - var _latitudeI: Int32 = 0 - var _longitudeI: Int32 = 0 - var _altitude: Int32 = 0 + var _latitudeI: Int32? = nil + var _longitudeI: Int32? = nil + var _altitude: Int32? = nil var _time: UInt32 = 0 var _locationSource: Position.LocSource = .locUnset var _altitudeSource: Position.AltSource = .altUnset var _timestamp: UInt32 = 0 var _timestampMillisAdjust: Int32 = 0 - var _altitudeHae: Int32 = 0 - var _altitudeGeoidalSeparation: Int32 = 0 + var _altitudeHae: Int32? = nil + var _altitudeGeoidalSeparation: Int32? = nil var _pdop: UInt32 = 0 var _hdop: UInt32 = 0 var _vdop: UInt32 = 0 var _gpsAccuracy: UInt32 = 0 - var _groundSpeed: UInt32 = 0 - var _groundTrack: UInt32 = 0 + var _groundSpeed: UInt32? = nil + var _groundTrack: UInt32? = nil var _fixQuality: UInt32 = 0 var _fixType: UInt32 = 0 var _satsInView: UInt32 = 0 @@ -2945,15 +3409,19 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._latitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: _storage._latitudeI, fieldNumber: 1) - } - if _storage._longitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: _storage._longitudeI, fieldNumber: 2) - } - if _storage._altitude != 0 { - try visitor.visitSingularInt32Field(value: _storage._altitude, fieldNumber: 3) - } + // 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 + try { if let v = _storage._latitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._longitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._altitude { + try visitor.visitSingularInt32Field(value: v, fieldNumber: 3) + } }() if _storage._time != 0 { try visitor.visitSingularFixed32Field(value: _storage._time, fieldNumber: 4) } @@ -2969,12 +3437,12 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._timestampMillisAdjust != 0 { try visitor.visitSingularInt32Field(value: _storage._timestampMillisAdjust, fieldNumber: 8) } - if _storage._altitudeHae != 0 { - try visitor.visitSingularSInt32Field(value: _storage._altitudeHae, fieldNumber: 9) - } - if _storage._altitudeGeoidalSeparation != 0 { - try visitor.visitSingularSInt32Field(value: _storage._altitudeGeoidalSeparation, fieldNumber: 10) - } + try { if let v = _storage._altitudeHae { + try visitor.visitSingularSInt32Field(value: v, fieldNumber: 9) + } }() + try { if let v = _storage._altitudeGeoidalSeparation { + try visitor.visitSingularSInt32Field(value: v, fieldNumber: 10) + } }() if _storage._pdop != 0 { try visitor.visitSingularUInt32Field(value: _storage._pdop, fieldNumber: 11) } @@ -2987,12 +3455,12 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._gpsAccuracy != 0 { try visitor.visitSingularUInt32Field(value: _storage._gpsAccuracy, fieldNumber: 14) } - if _storage._groundSpeed != 0 { - try visitor.visitSingularUInt32Field(value: _storage._groundSpeed, fieldNumber: 15) - } - if _storage._groundTrack != 0 { - try visitor.visitSingularUInt32Field(value: _storage._groundTrack, fieldNumber: 16) - } + try { if let v = _storage._groundSpeed { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 15) + } }() + try { if let v = _storage._groundTrack { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 16) + } }() if _storage._fixQuality != 0 { try visitor.visitSingularUInt32Field(value: _storage._fixQuality, fieldNumber: 17) } @@ -3084,6 +3552,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, 5: .standard(proto: "hw_model"), 6: .standard(proto: "is_licensed"), 7: .same(proto: "role"), + 8: .standard(proto: "public_key"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3099,6 +3568,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, case 5: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 8: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() default: break } } @@ -3126,6 +3596,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if self.role != .client { try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7) } + if !self.publicKey.isEmpty { + try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } @@ -3137,6 +3610,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if lhs.hwModel != rhs.hwModel {return false} if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} + if lhs.publicKey != rhs.publicKey {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3146,6 +3620,9 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement public static let protoMessageName: String = _protobuf_package + ".RouteDiscovery" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "route"), + 2: .standard(proto: "snr_towards"), + 3: .standard(proto: "route_back"), + 4: .standard(proto: "snr_back"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3155,6 +3632,9 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeRepeatedFixed32Field(value: &self.route) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.snrTowards) }() + case 3: try { try decoder.decodeRepeatedFixed32Field(value: &self.routeBack) }() + case 4: try { try decoder.decodeRepeatedInt32Field(value: &self.snrBack) }() default: break } } @@ -3164,11 +3644,23 @@ extension RouteDiscovery: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if !self.route.isEmpty { try visitor.visitPackedFixed32Field(value: self.route, fieldNumber: 1) } + if !self.snrTowards.isEmpty { + try visitor.visitPackedInt32Field(value: self.snrTowards, fieldNumber: 2) + } + if !self.routeBack.isEmpty { + try visitor.visitPackedFixed32Field(value: self.routeBack, fieldNumber: 3) + } + if !self.snrBack.isEmpty { + try visitor.visitPackedInt32Field(value: self.snrBack, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: RouteDiscovery, rhs: RouteDiscovery) -> Bool { if lhs.route != rhs.route {return false} + if lhs.snrTowards != rhs.snrTowards {return false} + if lhs.routeBack != rhs.routeBack {return false} + if lhs.snrBack != rhs.snrBack {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3271,6 +3763,8 @@ extension Routing.Error: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "DUTY_CYCLE_LIMIT"), 32: .same(proto: "BAD_REQUEST"), 33: .same(proto: "NOT_AUTHORIZED"), + 34: .same(proto: "PKI_FAILED"), + 35: .same(proto: "PKI_UNKNOWN_PUBKEY"), ] } @@ -3368,8 +3862,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.id) }() - case 2: try { try decoder.decodeSingularSFixed32Field(value: &self.latitudeI) }() - case 3: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() + case 2: try { try decoder.decodeSingularSFixed32Field(value: &self._latitudeI) }() + case 3: try { try decoder.decodeSingularSFixed32Field(value: &self._longitudeI) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self.expire) }() case 5: try { try decoder.decodeSingularUInt32Field(value: &self.lockedTo) }() case 6: try { try decoder.decodeSingularStringField(value: &self.name) }() @@ -3381,15 +3875,19 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB } 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.id != 0 { try visitor.visitSingularUInt32Field(value: self.id, fieldNumber: 1) } - if self.latitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: self.latitudeI, fieldNumber: 2) - } - if self.longitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: self.longitudeI, fieldNumber: 3) - } + try { if let v = self._latitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._longitudeI { + try visitor.visitSingularSFixed32Field(value: v, fieldNumber: 3) + } }() if self.expire != 0 { try visitor.visitSingularUInt32Field(value: self.expire, fieldNumber: 4) } @@ -3410,8 +3908,8 @@ extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static func ==(lhs: Waypoint, rhs: Waypoint) -> Bool { if lhs.id != rhs.id {return false} - if lhs.latitudeI != rhs.latitudeI {return false} - if lhs.longitudeI != rhs.longitudeI {return false} + if lhs._latitudeI != rhs._latitudeI {return false} + if lhs._longitudeI != rhs._longitudeI {return false} if lhs.expire != rhs.expire {return false} if lhs.lockedTo != rhs.lockedTo {return false} if lhs.name != rhs.name {return false} @@ -3512,6 +4010,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 13: .same(proto: "delayed"), 14: .standard(proto: "via_mqtt"), 15: .standard(proto: "hop_start"), + 16: .standard(proto: "public_key"), + 17: .standard(proto: "pki_encrypted"), ] fileprivate class _StorageClass { @@ -3529,6 +4029,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _delayed: MeshPacket.Delayed = .noDelay var _viaMqtt: Bool = false var _hopStart: UInt32 = 0 + var _publicKey: Data = Data() + var _pkiEncrypted: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -3557,6 +4059,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio _delayed = source._delayed _viaMqtt = source._viaMqtt _hopStart = source._hopStart + _publicKey = source._publicKey + _pkiEncrypted = source._pkiEncrypted } } @@ -3609,6 +4113,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 13: try { try decoder.decodeSingularEnumField(value: &_storage._delayed) }() case 14: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() case 15: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopStart) }() + case 16: try { try decoder.decodeSingularBytesField(value: &_storage._publicKey) }() + case 17: try { try decoder.decodeSingularBoolField(value: &_storage._pkiEncrypted) }() default: break } } @@ -3647,7 +4153,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr.bitPattern != 0 { + if _storage._rxSnr != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -3671,6 +4177,12 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._hopStart != 0 { try visitor.visitSingularUInt32Field(value: _storage._hopStart, fieldNumber: 15) } + if !_storage._publicKey.isEmpty { + try visitor.visitSingularBytesField(value: _storage._publicKey, fieldNumber: 16) + } + if _storage._pkiEncrypted != false { + try visitor.visitSingularBoolField(value: _storage._pkiEncrypted, fieldNumber: 17) + } } try unknownFields.traverse(visitor: &visitor) } @@ -3694,6 +4206,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._delayed != rhs_storage._delayed {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopStart != rhs_storage._hopStart {return false} + if _storage._publicKey != rhs_storage._publicKey {return false} + if _storage._pkiEncrypted != rhs_storage._pkiEncrypted {return false} return true } if !storagesAreEqual {return false} @@ -3822,7 +4336,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr.bitPattern != 0 { + if _storage._snr != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -4045,6 +4559,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 13: .same(proto: "metadata"), 14: .same(proto: "mqttClientProxyMessage"), 15: .same(proto: "fileInfo"), + 16: .same(proto: "clientNotification"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4226,6 +4741,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.payloadVariant = .fileInfo(v) } }() + case 16: try { + var v: ClientNotification? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .clientNotification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .clientNotification(v) + } + }() default: break } } @@ -4296,6 +4824,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .fileInfo(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 15) }() + case .clientNotification?: try { + guard case .clientNotification(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 16) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -4309,6 +4841,60 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation } } +extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ClientNotification" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "reply_id"), + 2: .same(proto: "time"), + 3: .same(proto: "level"), + 4: .same(proto: "message"), + ] + + 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._replyID) }() + 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) }() + 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 + try { if let v = self._replyID { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + if self.time != 0 { + try visitor.visitSingularFixed32Field(value: self.time, fieldNumber: 2) + } + if self.level != .unset { + try visitor.visitSingularEnumField(value: self.level, fieldNumber: 3) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: ClientNotification, rhs: ClientNotification) -> Bool { + if lhs._replyID != rhs._replyID {return false} + if lhs.time != rhs.time {return false} + if lhs.level != rhs.level {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension FileInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FileInfo" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -4595,7 +5181,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr.bitPattern != 0 { + if self.snr != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -4708,8 +5294,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 6f3b2d76..3186c349 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -58,18 +58,24 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] - } +#endif // swift(>=4.2) + /// /// Module Config -public struct ModuleConfig: Sendable { +public struct ModuleConfig { // 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. @@ -212,7 +218,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable, Sendable { + public enum OneOf_PayloadVariant: Equatable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -253,11 +259,73 @@ public struct ModuleConfig: Sendable { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) + #if !swift(>=4.1) + public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { + // 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 (lhs, rhs) { + case (.mqtt, .mqtt): return { + guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.serial, .serial): return { + guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.externalNotification, .externalNotification): return { + guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.storeForward, .storeForward): return { + guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rangeTest, .rangeTest): return { + guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.telemetry, .telemetry): return { + guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.cannedMessage, .cannedMessage): return { + guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.audio, .audio): return { + guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.remoteHardware, .remoteHardware): return { + guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.neighborInfo, .neighborInfo): return { + guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.ambientLighting, .ambientLighting): return { + guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.detectionSensor, .detectionSensor): return { + guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.paxcounter, .paxcounter): return { + guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// MQTT Client Config - public struct MQTTConfig: Sendable { + public struct MQTTConfig { // 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. @@ -332,7 +400,7 @@ public struct ModuleConfig: Sendable { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings: Sendable { + public struct MapReportSettings { // 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. @@ -352,7 +420,7 @@ public struct ModuleConfig: Sendable { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig: Sendable { + public struct RemoteHardwareConfig { // 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. @@ -376,7 +444,7 @@ public struct ModuleConfig: Sendable { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig: Sendable { + public struct NeighborInfoConfig { // 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. @@ -397,7 +465,7 @@ public struct ModuleConfig: Sendable { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig: Sendable { + public struct DetectionSensorConfig { // 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. @@ -448,7 +516,7 @@ public struct ModuleConfig: Sendable { /// /// Audio Config for codec2 voice - public struct AudioConfig: Sendable { + public struct AudioConfig { // 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. @@ -485,7 +553,7 @@ public struct ModuleConfig: Sendable { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Audio_Baud: SwiftProtobuf.Enum { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -532,19 +600,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] - } public init() {} @@ -552,7 +607,7 @@ public struct ModuleConfig: Sendable { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig: Sendable { + public struct PaxcounterConfig { // 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. @@ -578,7 +633,7 @@ public struct ModuleConfig: Sendable { /// /// Serial Config - public struct SerialConfig: Sendable { + public struct SerialConfig { // 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. @@ -621,7 +676,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Serial_Baud: SwiftProtobuf.Enum { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -689,31 +744,11 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] - } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Serial_Mode: SwiftProtobuf.Enum { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -758,17 +793,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] - } public init() {} @@ -776,7 +800,7 @@ public struct ModuleConfig: Sendable { /// /// External Notifications Config - public struct ExternalNotificationConfig: Sendable { + public struct ExternalNotificationConfig { // 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. @@ -859,7 +883,7 @@ public struct ModuleConfig: Sendable { /// /// Store and Forward Module Config - public struct StoreForwardConfig: Sendable { + public struct StoreForwardConfig { // 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. @@ -895,7 +919,7 @@ public struct ModuleConfig: Sendable { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig: Sendable { + public struct RangeTestConfig { // 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. @@ -920,7 +944,7 @@ public struct ModuleConfig: Sendable { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig: Sendable { + public struct TelemetryConfig { // 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. @@ -977,7 +1001,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public struct CannedMessageConfig: Sendable { + public struct CannedMessageConfig { // 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. @@ -1020,7 +1044,7 @@ public struct ModuleConfig: Sendable { /// /// Input event origin accepted by the canned message module. - /// Can be e.g. "rotEnc1", "upDownEnc1" or keyword "_any" + /// Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any" public var allowInputSource: String = String() /// @@ -1032,7 +1056,7 @@ public struct ModuleConfig: Sendable { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum InputEventChar: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -1100,18 +1124,6 @@ public struct ModuleConfig: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] - } public init() {} @@ -1120,7 +1132,7 @@ public struct ModuleConfig: Sendable { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig: Sendable { + public struct AmbientLightingConfig { // 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. @@ -1153,9 +1165,77 @@ public struct ModuleConfig: Sendable { public init() {} } +#if swift(>=4.2) + +extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] +} + +extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] +} + +extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + ] +} + +extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] +} + +#endif // swift(>=4.2) + /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin: Sendable { +public struct RemoteHardwarePin { // 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. @@ -1177,6 +1257,31 @@ public struct RemoteHardwarePin: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension RemoteHardwarePinType: @unchecked Sendable {} +extension ModuleConfig: @unchecked Sendable {} +extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} +extension ModuleConfig.MQTTConfig: @unchecked Sendable {} +extension ModuleConfig.MapReportSettings: @unchecked Sendable {} +extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} +extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} +extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} +extension ModuleConfig.AudioConfig: @unchecked Sendable {} +extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} +extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} +extension ModuleConfig.SerialConfig: @unchecked Sendable {} +extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} +extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} +extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} +extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} +extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} +extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} +extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} +extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} +extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} +extension RemoteHardwarePin: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index fc5e37a1..efe6cdd5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope: Sendable { +public struct ServiceEnvelope { // 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope: Sendable { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport: Sendable { +public struct MapReport { // 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. @@ -121,6 +121,11 @@ public struct MapReport: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension ServiceEnvelope: @unchecked Sendable {} +extension MapReport: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index f82b3c51..cf8aa463 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount: Sendable { +public struct Paxcount { // 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. @@ -44,6 +44,10 @@ public struct Paxcount: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Paxcount: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index c5348a8a..dd7e036f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum PortNum: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -167,7 +167,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// Provides a traceroute functionality to show the route a packet towards - /// a certain destination would take on the mesh. + /// a certain destination would take on the mesh. Contains a RouteDiscovery message as payload. /// ENCODING: Protobuf case tracerouteApp // = 70 @@ -277,6 +277,11 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { } } +} + +#if swift(>=4.2) + +extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -308,9 +313,14 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { .atakForwarder, .max, ] - } +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PortNum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 9c61e6d0..5f51e948 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon: Sendable { +public struct PowerMon { // 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. @@ -31,7 +31,7 @@ public struct PowerMon: Sendable { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum State: SwiftProtobuf.Enum { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,31 +104,37 @@ public struct PowerMon: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] - } public init() {} } +#if swift(>=4.2) + +extension PowerMon.State: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] +} + +#endif // swift(>=4.2) + /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage: Sendable { +public struct PowerStressMessage { // 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. @@ -145,7 +151,7 @@ public struct PowerStressMessage: Sendable { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Opcode: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -266,35 +272,48 @@ public struct PowerStressMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] - } public init() {} } +#if swift(>=4.2) + +extension PowerStressMessage.Opcode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PowerMon: @unchecked Sendable {} +extension PowerMon.State: @unchecked Sendable {} +extension PowerStressMessage: @unchecked Sendable {} +extension PowerStressMessage.Opcode: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -304,8 +323,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} + while let _ = try decoder.nextFieldNumber() { + } } public func traverse(visitor: inout V) throws { @@ -360,7 +379,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds.bitPattern != 0 { + if self.numSeconds != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index 60f64504..ac6eeb26 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage: Sendable { +public struct HardwareMessage { // 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. @@ -52,7 +52,7 @@ public struct HardwareMessage: Sendable { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum TypeEnum: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -110,21 +110,32 @@ public struct HardwareMessage: Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] - } public init() {} } +#if swift(>=4.2) + +extension HardwareMessage.TypeEnum: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension HardwareMessage: @unchecked Sendable {} +extension HardwareMessage.TypeEnum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index c1f3f678..6fdf3208 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig: Sendable { +public struct RTTTLConfig { // 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. @@ -36,6 +36,10 @@ public struct RTTTLConfig: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension RTTTLConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 0b67eaf6..54efa77b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward: @unchecked Sendable { +public struct StoreAndForward { // 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. @@ -79,7 +79,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable, @unchecked Sendable { + public enum OneOf_Variant: Equatable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,12 +93,38 @@ public struct StoreAndForward: @unchecked Sendable { /// Text from history message. case text(Data) + #if !swift(>=4.1) + public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.stats, .stats): return { + guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.history, .history): return { + guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum RequestResponse: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -216,31 +242,11 @@ public struct StoreAndForward: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] - } /// /// TODO: REPLACE - public struct Statistics: Sendable { + public struct Statistics { // 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. @@ -288,7 +294,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public struct History: Sendable { + public struct History { // 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. @@ -313,7 +319,7 @@ public struct StoreAndForward: @unchecked Sendable { /// /// TODO: REPLACE - public struct Heartbeat: Sendable { + public struct Heartbeat { // 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. @@ -334,6 +340,41 @@ public struct StoreAndForward: @unchecked Sendable { public init() {} } +#if swift(>=4.2) + +extension StoreAndForward.RequestResponse: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension StoreAndForward: @unchecked Sendable {} +extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} +extension StoreAndForward.RequestResponse: @unchecked Sendable {} +extension StoreAndForward.Statistics: @unchecked Sendable {} +extension StoreAndForward.History: @unchecked Sendable {} +extension StoreAndForward.Heartbeat: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 7a9b81de..e4b9ee08 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { +public enum TelemetrySensorType: SwiftProtobuf.Enum { public typealias RawValue = Int /// @@ -128,6 +128,18 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// NAU7802 Scale Chip or compatible case nau7802 // = 25 + + /// + /// BMP3XX High accuracy temperature and pressure + case bmp3Xx // = 26 + + /// + /// ICM-20948 9-Axis digital motion processor + case icm20948 // = 27 + + /// + /// MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) + case max17048 // = 28 case UNRECOGNIZED(Int) public init() { @@ -162,6 +174,9 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case 23: self = .aht10 case 24: self = .dfrobotLark case 25: self = .nau7802 + case 26: self = .bmp3Xx + case 27: self = .icm20948 + case 28: self = .max17048 default: self = .UNRECOGNIZED(rawValue) } } @@ -194,10 +209,18 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case .aht10: return 23 case .dfrobotLark: return 24 case .nau7802: return 25 + case .bmp3Xx: return 26 + case .icm20948: return 27 + case .max17048: return 28 case .UNRECOGNIZED(let i): return i } } +} + +#if swift(>=4.2) + +extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -226,45 +249,90 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .aht10, .dfrobotLark, .nau7802, + .bmp3Xx, + .icm20948, + .max17048, ] - } +#endif // swift(>=4.2) + /// /// Key native device metrics such as battery level -public struct DeviceMetrics: Sendable { +public struct DeviceMetrics { // 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. /// /// 0-100 (>100 means powered) - public var batteryLevel: UInt32 = 0 + public var batteryLevel: UInt32 { + get {return _batteryLevel ?? 0} + set {_batteryLevel = newValue} + } + /// Returns true if `batteryLevel` has been explicitly set. + public var hasBatteryLevel: Bool {return self._batteryLevel != nil} + /// Clears the value of `batteryLevel`. Subsequent reads from it will return its default value. + public mutating func clearBatteryLevel() {self._batteryLevel = nil} /// /// Voltage measured - public var voltage: Float = 0 + public var voltage: Float { + get {return _voltage ?? 0} + set {_voltage = newValue} + } + /// Returns true if `voltage` has been explicitly set. + public var hasVoltage: Bool {return self._voltage != nil} + /// Clears the value of `voltage`. Subsequent reads from it will return its default value. + public mutating func clearVoltage() {self._voltage = nil} /// /// Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). - public var channelUtilization: Float = 0 + public var channelUtilization: Float { + get {return _channelUtilization ?? 0} + set {_channelUtilization = newValue} + } + /// Returns true if `channelUtilization` has been explicitly set. + public var hasChannelUtilization: Bool {return self._channelUtilization != nil} + /// Clears the value of `channelUtilization`. Subsequent reads from it will return its default value. + public mutating func clearChannelUtilization() {self._channelUtilization = nil} /// /// Percent of airtime for transmission used within the last hour. - public var airUtilTx: Float = 0 + public var airUtilTx: Float { + get {return _airUtilTx ?? 0} + set {_airUtilTx = newValue} + } + /// Returns true if `airUtilTx` has been explicitly set. + public var hasAirUtilTx: Bool {return self._airUtilTx != nil} + /// Clears the value of `airUtilTx`. Subsequent reads from it will return its default value. + public mutating func clearAirUtilTx() {self._airUtilTx = nil} /// /// How long the device has been running since the last reboot (in seconds) - public var uptimeSeconds: UInt32 = 0 + public var uptimeSeconds: UInt32 { + get {return _uptimeSeconds ?? 0} + set {_uptimeSeconds = newValue} + } + /// Returns true if `uptimeSeconds` has been explicitly set. + public var hasUptimeSeconds: Bool {return self._uptimeSeconds != nil} + /// Clears the value of `uptimeSeconds`. Subsequent reads from it will return its default value. + public mutating func clearUptimeSeconds() {self._uptimeSeconds = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _batteryLevel: UInt32? = nil + fileprivate var _voltage: Float? = nil + fileprivate var _channelUtilization: Float? = nil + fileprivate var _airUtilTx: Float? = nil + fileprivate var _uptimeSeconds: UInt32? = nil } /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics: @unchecked Sendable { +public struct EnvironmentMetrics { // 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. @@ -272,123 +340,191 @@ public struct EnvironmentMetrics: @unchecked Sendable { /// /// Temperature measured public var temperature: Float { - get {return _storage._temperature} + get {return _storage._temperature ?? 0} set {_uniqueStorage()._temperature = newValue} } + /// Returns true if `temperature` has been explicitly set. + public var hasTemperature: Bool {return _storage._temperature != nil} + /// Clears the value of `temperature`. Subsequent reads from it will return its default value. + public mutating func clearTemperature() {_uniqueStorage()._temperature = nil} /// /// Relative humidity percent measured public var relativeHumidity: Float { - get {return _storage._relativeHumidity} + get {return _storage._relativeHumidity ?? 0} set {_uniqueStorage()._relativeHumidity = newValue} } + /// Returns true if `relativeHumidity` has been explicitly set. + public var hasRelativeHumidity: Bool {return _storage._relativeHumidity != nil} + /// Clears the value of `relativeHumidity`. Subsequent reads from it will return its default value. + public mutating func clearRelativeHumidity() {_uniqueStorage()._relativeHumidity = nil} /// /// Barometric pressure in hPA measured public var barometricPressure: Float { - get {return _storage._barometricPressure} + get {return _storage._barometricPressure ?? 0} set {_uniqueStorage()._barometricPressure = newValue} } + /// Returns true if `barometricPressure` has been explicitly set. + public var hasBarometricPressure: Bool {return _storage._barometricPressure != nil} + /// Clears the value of `barometricPressure`. Subsequent reads from it will return its default value. + public mutating func clearBarometricPressure() {_uniqueStorage()._barometricPressure = nil} /// /// Gas resistance in MOhm measured public var gasResistance: Float { - get {return _storage._gasResistance} + get {return _storage._gasResistance ?? 0} set {_uniqueStorage()._gasResistance = newValue} } + /// Returns true if `gasResistance` has been explicitly set. + public var hasGasResistance: Bool {return _storage._gasResistance != nil} + /// Clears the value of `gasResistance`. Subsequent reads from it will return its default value. + public mutating func clearGasResistance() {_uniqueStorage()._gasResistance = nil} /// /// Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) public var voltage: Float { - get {return _storage._voltage} + get {return _storage._voltage ?? 0} set {_uniqueStorage()._voltage = newValue} } + /// Returns true if `voltage` has been explicitly set. + public var hasVoltage: Bool {return _storage._voltage != nil} + /// Clears the value of `voltage`. Subsequent reads from it will return its default value. + public mutating func clearVoltage() {_uniqueStorage()._voltage = nil} /// /// Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) public var current: Float { - get {return _storage._current} + get {return _storage._current ?? 0} set {_uniqueStorage()._current = newValue} } + /// Returns true if `current` has been explicitly set. + public var hasCurrent: Bool {return _storage._current != nil} + /// Clears the value of `current`. Subsequent reads from it will return its default value. + public mutating func clearCurrent() {_uniqueStorage()._current = nil} /// /// relative scale IAQ value as measured by Bosch BME680 . value 0-500. /// Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. public var iaq: UInt32 { - get {return _storage._iaq} + get {return _storage._iaq ?? 0} set {_uniqueStorage()._iaq = newValue} } + /// Returns true if `iaq` has been explicitly set. + public var hasIaq: Bool {return _storage._iaq != nil} + /// Clears the value of `iaq`. Subsequent reads from it will return its default value. + public mutating func clearIaq() {_uniqueStorage()._iaq = nil} /// /// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. public var distance: Float { - get {return _storage._distance} + get {return _storage._distance ?? 0} set {_uniqueStorage()._distance = newValue} } + /// Returns true if `distance` has been explicitly set. + public var hasDistance: Bool {return _storage._distance != nil} + /// Clears the value of `distance`. Subsequent reads from it will return its default value. + public mutating func clearDistance() {_uniqueStorage()._distance = nil} /// /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. public var lux: Float { - get {return _storage._lux} + get {return _storage._lux ?? 0} set {_uniqueStorage()._lux = newValue} } + /// Returns true if `lux` has been explicitly set. + public var hasLux: Bool {return _storage._lux != nil} + /// Clears the value of `lux`. Subsequent reads from it will return its default value. + public mutating func clearLux() {_uniqueStorage()._lux = nil} /// /// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. public var whiteLux: Float { - get {return _storage._whiteLux} + get {return _storage._whiteLux ?? 0} set {_uniqueStorage()._whiteLux = newValue} } + /// Returns true if `whiteLux` has been explicitly set. + public var hasWhiteLux: Bool {return _storage._whiteLux != nil} + /// Clears the value of `whiteLux`. Subsequent reads from it will return its default value. + public mutating func clearWhiteLux() {_uniqueStorage()._whiteLux = nil} /// /// Infrared lux public var irLux: Float { - get {return _storage._irLux} + get {return _storage._irLux ?? 0} set {_uniqueStorage()._irLux = newValue} } + /// Returns true if `irLux` has been explicitly set. + public var hasIrLux: Bool {return _storage._irLux != nil} + /// Clears the value of `irLux`. Subsequent reads from it will return its default value. + public mutating func clearIrLux() {_uniqueStorage()._irLux = nil} /// /// Ultraviolet lux public var uvLux: Float { - get {return _storage._uvLux} + get {return _storage._uvLux ?? 0} set {_uniqueStorage()._uvLux = newValue} } + /// Returns true if `uvLux` has been explicitly set. + public var hasUvLux: Bool {return _storage._uvLux != nil} + /// Clears the value of `uvLux`. Subsequent reads from it will return its default value. + public mutating func clearUvLux() {_uniqueStorage()._uvLux = nil} /// /// Wind direction in degrees /// 0 degrees = North, 90 = East, etc... public var windDirection: UInt32 { - get {return _storage._windDirection} + get {return _storage._windDirection ?? 0} set {_uniqueStorage()._windDirection = newValue} } + /// Returns true if `windDirection` has been explicitly set. + public var hasWindDirection: Bool {return _storage._windDirection != nil} + /// Clears the value of `windDirection`. Subsequent reads from it will return its default value. + public mutating func clearWindDirection() {_uniqueStorage()._windDirection = nil} /// /// Wind speed in m/s public var windSpeed: Float { - get {return _storage._windSpeed} + get {return _storage._windSpeed ?? 0} set {_uniqueStorage()._windSpeed = newValue} } + /// Returns true if `windSpeed` has been explicitly set. + public var hasWindSpeed: Bool {return _storage._windSpeed != nil} + /// Clears the value of `windSpeed`. Subsequent reads from it will return its default value. + public mutating func clearWindSpeed() {_uniqueStorage()._windSpeed = nil} /// /// Weight in KG public var weight: Float { - get {return _storage._weight} + get {return _storage._weight ?? 0} set {_uniqueStorage()._weight = newValue} } + /// Returns true if `weight` has been explicitly set. + public var hasWeight: Bool {return _storage._weight != nil} + /// Clears the value of `weight`. Subsequent reads from it will return its default value. + public mutating func clearWeight() {_uniqueStorage()._weight = nil} /// /// Wind gust in m/s public var windGust: Float { - get {return _storage._windGust} + get {return _storage._windGust ?? 0} set {_uniqueStorage()._windGust = newValue} } + /// Returns true if `windGust` has been explicitly set. + public var hasWindGust: Bool {return _storage._windGust != nil} + /// Clears the value of `windGust`. Subsequent reads from it will return its default value. + public mutating func clearWindGust() {_uniqueStorage()._windGust = nil} /// /// Wind lull in m/s public var windLull: Float { - get {return _storage._windLull} + get {return _storage._windLull ?? 0} set {_uniqueStorage()._windLull = newValue} } + /// Returns true if `windLull` has been explicitly set. + public var hasWindLull: Bool {return _storage._windLull != nil} + /// Clears the value of `windLull`. Subsequent reads from it will return its default value. + public mutating func clearWindLull() {_uniqueStorage()._windLull = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -399,94 +535,284 @@ public struct EnvironmentMetrics: @unchecked Sendable { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics: Sendable { +public struct PowerMetrics { // 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. /// /// Voltage (Ch1) - public var ch1Voltage: Float = 0 + public var ch1Voltage: Float { + get {return _ch1Voltage ?? 0} + set {_ch1Voltage = newValue} + } + /// Returns true if `ch1Voltage` has been explicitly set. + public var hasCh1Voltage: Bool {return self._ch1Voltage != nil} + /// Clears the value of `ch1Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh1Voltage() {self._ch1Voltage = nil} /// /// Current (Ch1) - public var ch1Current: Float = 0 + public var ch1Current: Float { + get {return _ch1Current ?? 0} + set {_ch1Current = newValue} + } + /// Returns true if `ch1Current` has been explicitly set. + public var hasCh1Current: Bool {return self._ch1Current != nil} + /// Clears the value of `ch1Current`. Subsequent reads from it will return its default value. + public mutating func clearCh1Current() {self._ch1Current = nil} /// /// Voltage (Ch2) - public var ch2Voltage: Float = 0 + public var ch2Voltage: Float { + get {return _ch2Voltage ?? 0} + set {_ch2Voltage = newValue} + } + /// Returns true if `ch2Voltage` has been explicitly set. + public var hasCh2Voltage: Bool {return self._ch2Voltage != nil} + /// Clears the value of `ch2Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh2Voltage() {self._ch2Voltage = nil} /// /// Current (Ch2) - public var ch2Current: Float = 0 + public var ch2Current: Float { + get {return _ch2Current ?? 0} + set {_ch2Current = newValue} + } + /// Returns true if `ch2Current` has been explicitly set. + public var hasCh2Current: Bool {return self._ch2Current != nil} + /// Clears the value of `ch2Current`. Subsequent reads from it will return its default value. + public mutating func clearCh2Current() {self._ch2Current = nil} /// /// Voltage (Ch3) - public var ch3Voltage: Float = 0 + public var ch3Voltage: Float { + get {return _ch3Voltage ?? 0} + set {_ch3Voltage = newValue} + } + /// Returns true if `ch3Voltage` has been explicitly set. + public var hasCh3Voltage: Bool {return self._ch3Voltage != nil} + /// Clears the value of `ch3Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh3Voltage() {self._ch3Voltage = nil} /// /// Current (Ch3) - public var ch3Current: Float = 0 + public var ch3Current: Float { + get {return _ch3Current ?? 0} + set {_ch3Current = newValue} + } + /// Returns true if `ch3Current` has been explicitly set. + public var hasCh3Current: Bool {return self._ch3Current != nil} + /// Clears the value of `ch3Current`. Subsequent reads from it will return its default value. + public mutating func clearCh3Current() {self._ch3Current = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _ch1Voltage: Float? = nil + fileprivate var _ch1Current: Float? = nil + fileprivate var _ch2Voltage: Float? = nil + fileprivate var _ch2Current: Float? = nil + fileprivate var _ch3Voltage: Float? = nil + fileprivate var _ch3Current: Float? = nil } /// /// Air quality metrics -public struct AirQualityMetrics: Sendable { +public struct AirQualityMetrics { // 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. /// /// Concentration Units Standard PM1.0 - public var pm10Standard: UInt32 = 0 + public var pm10Standard: UInt32 { + get {return _pm10Standard ?? 0} + set {_pm10Standard = newValue} + } + /// Returns true if `pm10Standard` has been explicitly set. + public var hasPm10Standard: Bool {return self._pm10Standard != nil} + /// Clears the value of `pm10Standard`. Subsequent reads from it will return its default value. + public mutating func clearPm10Standard() {self._pm10Standard = nil} /// /// Concentration Units Standard PM2.5 - public var pm25Standard: UInt32 = 0 + public var pm25Standard: UInt32 { + get {return _pm25Standard ?? 0} + set {_pm25Standard = newValue} + } + /// Returns true if `pm25Standard` has been explicitly set. + public var hasPm25Standard: Bool {return self._pm25Standard != nil} + /// Clears the value of `pm25Standard`. Subsequent reads from it will return its default value. + public mutating func clearPm25Standard() {self._pm25Standard = nil} /// /// Concentration Units Standard PM10.0 - public var pm100Standard: UInt32 = 0 + public var pm100Standard: UInt32 { + get {return _pm100Standard ?? 0} + set {_pm100Standard = newValue} + } + /// Returns true if `pm100Standard` has been explicitly set. + public var hasPm100Standard: Bool {return self._pm100Standard != nil} + /// Clears the value of `pm100Standard`. Subsequent reads from it will return its default value. + public mutating func clearPm100Standard() {self._pm100Standard = nil} /// /// Concentration Units Environmental PM1.0 - public var pm10Environmental: UInt32 = 0 + public var pm10Environmental: UInt32 { + get {return _pm10Environmental ?? 0} + set {_pm10Environmental = newValue} + } + /// Returns true if `pm10Environmental` has been explicitly set. + public var hasPm10Environmental: Bool {return self._pm10Environmental != nil} + /// Clears the value of `pm10Environmental`. Subsequent reads from it will return its default value. + public mutating func clearPm10Environmental() {self._pm10Environmental = nil} /// /// Concentration Units Environmental PM2.5 - public var pm25Environmental: UInt32 = 0 + public var pm25Environmental: UInt32 { + get {return _pm25Environmental ?? 0} + set {_pm25Environmental = newValue} + } + /// Returns true if `pm25Environmental` has been explicitly set. + public var hasPm25Environmental: Bool {return self._pm25Environmental != nil} + /// Clears the value of `pm25Environmental`. Subsequent reads from it will return its default value. + public mutating func clearPm25Environmental() {self._pm25Environmental = nil} /// /// Concentration Units Environmental PM10.0 - public var pm100Environmental: UInt32 = 0 + public var pm100Environmental: UInt32 { + get {return _pm100Environmental ?? 0} + set {_pm100Environmental = newValue} + } + /// Returns true if `pm100Environmental` has been explicitly set. + public var hasPm100Environmental: Bool {return self._pm100Environmental != nil} + /// Clears the value of `pm100Environmental`. Subsequent reads from it will return its default value. + public mutating func clearPm100Environmental() {self._pm100Environmental = nil} /// /// 0.3um Particle Count - public var particles03Um: UInt32 = 0 + public var particles03Um: UInt32 { + get {return _particles03Um ?? 0} + set {_particles03Um = newValue} + } + /// Returns true if `particles03Um` has been explicitly set. + public var hasParticles03Um: Bool {return self._particles03Um != nil} + /// Clears the value of `particles03Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles03Um() {self._particles03Um = nil} /// /// 0.5um Particle Count - public var particles05Um: UInt32 = 0 + public var particles05Um: UInt32 { + get {return _particles05Um ?? 0} + set {_particles05Um = newValue} + } + /// Returns true if `particles05Um` has been explicitly set. + public var hasParticles05Um: Bool {return self._particles05Um != nil} + /// Clears the value of `particles05Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles05Um() {self._particles05Um = nil} /// /// 1.0um Particle Count - public var particles10Um: UInt32 = 0 + public var particles10Um: UInt32 { + get {return _particles10Um ?? 0} + set {_particles10Um = newValue} + } + /// Returns true if `particles10Um` has been explicitly set. + public var hasParticles10Um: Bool {return self._particles10Um != nil} + /// Clears the value of `particles10Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles10Um() {self._particles10Um = nil} /// /// 2.5um Particle Count - public var particles25Um: UInt32 = 0 + public var particles25Um: UInt32 { + get {return _particles25Um ?? 0} + set {_particles25Um = newValue} + } + /// Returns true if `particles25Um` has been explicitly set. + public var hasParticles25Um: Bool {return self._particles25Um != nil} + /// Clears the value of `particles25Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles25Um() {self._particles25Um = nil} /// /// 5.0um Particle Count - public var particles50Um: UInt32 = 0 + public var particles50Um: UInt32 { + get {return _particles50Um ?? 0} + set {_particles50Um = newValue} + } + /// Returns true if `particles50Um` has been explicitly set. + public var hasParticles50Um: Bool {return self._particles50Um != nil} + /// Clears the value of `particles50Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles50Um() {self._particles50Um = nil} /// /// 10.0um Particle Count - public var particles100Um: UInt32 = 0 + public var particles100Um: UInt32 { + get {return _particles100Um ?? 0} + set {_particles100Um = newValue} + } + /// Returns true if `particles100Um` has been explicitly set. + public var hasParticles100Um: Bool {return self._particles100Um != nil} + /// Clears the value of `particles100Um`. Subsequent reads from it will return its default value. + public mutating func clearParticles100Um() {self._particles100Um = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _pm10Standard: UInt32? = nil + fileprivate var _pm25Standard: UInt32? = nil + fileprivate var _pm100Standard: UInt32? = nil + fileprivate var _pm10Environmental: UInt32? = nil + fileprivate var _pm25Environmental: UInt32? = nil + fileprivate var _pm100Environmental: UInt32? = nil + fileprivate var _particles03Um: UInt32? = nil + fileprivate var _particles05Um: UInt32? = nil + fileprivate var _particles10Um: UInt32? = nil + fileprivate var _particles25Um: UInt32? = nil + fileprivate var _particles50Um: UInt32? = nil + fileprivate var _particles100Um: UInt32? = nil +} + +/// +/// Local device mesh statistics +public struct LocalStats { + // 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. + + /// + /// How long the device has been running since the last reboot (in seconds) + public var uptimeSeconds: UInt32 = 0 + + /// + /// Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). + public var channelUtilization: Float = 0 + + /// + /// Percent of airtime for transmission used within the last hour. + public var airUtilTx: Float = 0 + + /// + /// Number of packets sent + public var numPacketsTx: UInt32 = 0 + + /// + /// Number of packets received good + public var numPacketsRx: UInt32 = 0 + + /// + /// Number of packets received that are malformed or violate the protocol + public var numPacketsRxBad: UInt32 = 0 + + /// + /// Number of nodes online (in the past 2 hours) + public var numOnlineNodes: UInt32 = 0 + + /// + /// Number of nodes total + public var numTotalNodes: UInt32 = 0 public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -495,7 +821,7 @@ public struct AirQualityMetrics: Sendable { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry: Sendable { +public struct Telemetry { // 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. @@ -546,9 +872,19 @@ public struct Telemetry: Sendable { set {variant = .powerMetrics(newValue)} } + /// + /// Local device mesh statistics + public var localStats: LocalStats { + get { + if case .localStats(let v)? = variant {return v} + return LocalStats() + } + set {variant = .localStats(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable, Sendable { + public enum OneOf_Variant: Equatable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -561,7 +897,40 @@ public struct Telemetry: Sendable { /// /// Power Metrics case powerMetrics(PowerMetrics) + /// + /// Local device mesh statistics + case localStats(LocalStats) + #if !swift(>=4.1) + public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { + // 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 (lhs, rhs) { + case (.deviceMetrics, .deviceMetrics): return { + guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.environmentMetrics, .environmentMetrics): return { + guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.airQualityMetrics, .airQualityMetrics): return { + guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.powerMetrics, .powerMetrics): return { + guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.localStats, .localStats): return { + guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif } public init() {} @@ -569,7 +938,7 @@ public struct Telemetry: Sendable { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config: Sendable { +public struct Nau7802Config { // 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. @@ -587,6 +956,18 @@ public struct Nau7802Config: Sendable { public init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension TelemetrySensorType: @unchecked Sendable {} +extension DeviceMetrics: @unchecked Sendable {} +extension EnvironmentMetrics: @unchecked Sendable {} +extension PowerMetrics: @unchecked Sendable {} +extension AirQualityMetrics: @unchecked Sendable {} +extension LocalStats: @unchecked Sendable {} +extension Telemetry: @unchecked Sendable {} +extension Telemetry.OneOf_Variant: @unchecked Sendable {} +extension Nau7802Config: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -619,6 +1000,9 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 23: .same(proto: "AHT10"), 24: .same(proto: "DFROBOT_LARK"), 25: .same(proto: "NAU7802"), + 26: .same(proto: "BMP3XX"), + 27: .same(proto: "ICM20948"), + 28: .same(proto: "MAX17048"), ] } @@ -638,41 +1022,45 @@ extension DeviceMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa // 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.batteryLevel) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.voltage) }() - case 3: try { try decoder.decodeSingularFloatField(value: &self.channelUtilization) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.airUtilTx) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self.uptimeSeconds) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._batteryLevel) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self._voltage) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self._channelUtilization) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self._airUtilTx) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self._uptimeSeconds) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.batteryLevel != 0 { - try visitor.visitSingularUInt32Field(value: self.batteryLevel, fieldNumber: 1) - } - if self.voltage.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.voltage, fieldNumber: 2) - } - if self.channelUtilization.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 3) - } - if self.airUtilTx.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 4) - } - if self.uptimeSeconds != 0 { - try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 5) - } + // 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 + try { if let v = self._batteryLevel { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 2) + } }() + try { if let v = self._channelUtilization { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try { if let v = self._airUtilTx { + try visitor.visitSingularFloatField(value: v, fieldNumber: 4) + } }() + try { if let v = self._uptimeSeconds { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: DeviceMetrics, rhs: DeviceMetrics) -> Bool { - if lhs.batteryLevel != rhs.batteryLevel {return false} - if lhs.voltage != rhs.voltage {return false} - if lhs.channelUtilization != rhs.channelUtilization {return false} - if lhs.airUtilTx != rhs.airUtilTx {return false} - if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs._batteryLevel != rhs._batteryLevel {return false} + if lhs._voltage != rhs._voltage {return false} + if lhs._channelUtilization != rhs._channelUtilization {return false} + if lhs._airUtilTx != rhs._airUtilTx {return false} + if lhs._uptimeSeconds != rhs._uptimeSeconds {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -701,23 +1089,23 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple ] fileprivate class _StorageClass { - var _temperature: Float = 0 - var _relativeHumidity: Float = 0 - var _barometricPressure: Float = 0 - var _gasResistance: Float = 0 - var _voltage: Float = 0 - var _current: Float = 0 - var _iaq: UInt32 = 0 - var _distance: Float = 0 - var _lux: Float = 0 - var _whiteLux: Float = 0 - var _irLux: Float = 0 - var _uvLux: Float = 0 - var _windDirection: UInt32 = 0 - var _windSpeed: Float = 0 - var _weight: Float = 0 - var _windGust: Float = 0 - var _windLull: Float = 0 + var _temperature: Float? = nil + var _relativeHumidity: Float? = nil + var _barometricPressure: Float? = nil + var _gasResistance: Float? = nil + var _voltage: Float? = nil + var _current: Float? = nil + var _iaq: UInt32? = nil + var _distance: Float? = nil + var _lux: Float? = nil + var _whiteLux: Float? = nil + var _irLux: Float? = nil + var _uvLux: Float? = nil + var _windDirection: UInt32? = nil + var _windSpeed: Float? = nil + var _weight: Float? = nil + var _windGust: Float? = nil + var _windLull: Float? = nil #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -792,57 +1180,61 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._temperature.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._temperature, fieldNumber: 1) - } - if _storage._relativeHumidity.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._relativeHumidity, fieldNumber: 2) - } - if _storage._barometricPressure.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._barometricPressure, fieldNumber: 3) - } - if _storage._gasResistance.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._gasResistance, fieldNumber: 4) - } - if _storage._voltage.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._voltage, fieldNumber: 5) - } - if _storage._current.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._current, fieldNumber: 6) - } - if _storage._iaq != 0 { - try visitor.visitSingularUInt32Field(value: _storage._iaq, fieldNumber: 7) - } - if _storage._distance.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._distance, fieldNumber: 8) - } - if _storage._lux.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._lux, fieldNumber: 9) - } - if _storage._whiteLux.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._whiteLux, fieldNumber: 10) - } - if _storage._irLux.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._irLux, fieldNumber: 11) - } - if _storage._uvLux.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._uvLux, fieldNumber: 12) - } - if _storage._windDirection != 0 { - try visitor.visitSingularUInt32Field(value: _storage._windDirection, fieldNumber: 13) - } - if _storage._windSpeed.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._windSpeed, fieldNumber: 14) - } - if _storage._weight.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._weight, fieldNumber: 15) - } - if _storage._windGust.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._windGust, fieldNumber: 16) - } - if _storage._windLull.bitPattern != 0 { - try visitor.visitSingularFloatField(value: _storage._windLull, fieldNumber: 17) - } + // 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 + try { if let v = _storage._temperature { + try visitor.visitSingularFloatField(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._relativeHumidity { + try visitor.visitSingularFloatField(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._barometricPressure { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try { if let v = _storage._gasResistance { + try visitor.visitSingularFloatField(value: v, fieldNumber: 4) + } }() + try { if let v = _storage._voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 5) + } }() + try { if let v = _storage._current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 6) + } }() + try { if let v = _storage._iaq { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + } }() + try { if let v = _storage._distance { + try visitor.visitSingularFloatField(value: v, fieldNumber: 8) + } }() + try { if let v = _storage._lux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 9) + } }() + try { if let v = _storage._whiteLux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 10) + } }() + try { if let v = _storage._irLux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 11) + } }() + try { if let v = _storage._uvLux { + try visitor.visitSingularFloatField(value: v, fieldNumber: 12) + } }() + try { if let v = _storage._windDirection { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 13) + } }() + try { if let v = _storage._windSpeed { + try visitor.visitSingularFloatField(value: v, fieldNumber: 14) + } }() + try { if let v = _storage._weight { + try visitor.visitSingularFloatField(value: v, fieldNumber: 15) + } }() + try { if let v = _storage._windGust { + try visitor.visitSingularFloatField(value: v, fieldNumber: 16) + } }() + try { if let v = _storage._windLull { + try visitor.visitSingularFloatField(value: v, fieldNumber: 17) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -895,46 +1287,50 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat // 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.decodeSingularFloatField(value: &self.ch1Voltage) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.ch1Current) }() - case 3: try { try decoder.decodeSingularFloatField(value: &self.ch2Voltage) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.ch2Current) }() - case 5: try { try decoder.decodeSingularFloatField(value: &self.ch3Voltage) }() - case 6: try { try decoder.decodeSingularFloatField(value: &self.ch3Current) }() + case 1: try { try decoder.decodeSingularFloatField(value: &self._ch1Voltage) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self._ch1Current) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self._ch2Voltage) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self._ch2Current) }() + case 5: try { try decoder.decodeSingularFloatField(value: &self._ch3Voltage) }() + case 6: try { try decoder.decodeSingularFloatField(value: &self._ch3Current) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.ch1Voltage.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.ch1Voltage, fieldNumber: 1) - } - if self.ch1Current.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.ch1Current, fieldNumber: 2) - } - if self.ch2Voltage.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.ch2Voltage, fieldNumber: 3) - } - if self.ch2Current.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.ch2Current, fieldNumber: 4) - } - if self.ch3Voltage.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.ch3Voltage, fieldNumber: 5) - } - if self.ch3Current.bitPattern != 0 { - try visitor.visitSingularFloatField(value: self.ch3Current, fieldNumber: 6) - } + // 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 + try { if let v = self._ch1Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 1) + } }() + try { if let v = self._ch1Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 2) + } }() + try { if let v = self._ch2Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try { if let v = self._ch2Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 4) + } }() + try { if let v = self._ch3Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 5) + } }() + try { if let v = self._ch3Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 6) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: PowerMetrics, rhs: PowerMetrics) -> Bool { - if lhs.ch1Voltage != rhs.ch1Voltage {return false} - if lhs.ch1Current != rhs.ch1Current {return false} - if lhs.ch2Voltage != rhs.ch2Voltage {return false} - if lhs.ch2Current != rhs.ch2Current {return false} - if lhs.ch3Voltage != rhs.ch3Voltage {return false} - if lhs.ch3Current != rhs.ch3Current {return false} + if lhs._ch1Voltage != rhs._ch1Voltage {return false} + if lhs._ch1Current != rhs._ch1Current {return false} + if lhs._ch2Voltage != rhs._ch2Voltage {return false} + if lhs._ch2Current != rhs._ch2Current {return false} + if lhs._ch3Voltage != rhs._ch3Voltage {return false} + if lhs._ch3Current != rhs._ch3Current {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -963,76 +1359,154 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem // 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.pm10Standard) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.pm25Standard) }() - case 3: try { try decoder.decodeSingularUInt32Field(value: &self.pm100Standard) }() - case 4: try { try decoder.decodeSingularUInt32Field(value: &self.pm10Environmental) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self.pm25Environmental) }() - case 6: try { try decoder.decodeSingularUInt32Field(value: &self.pm100Environmental) }() - case 7: try { try decoder.decodeSingularUInt32Field(value: &self.particles03Um) }() - case 8: try { try decoder.decodeSingularUInt32Field(value: &self.particles05Um) }() - case 9: try { try decoder.decodeSingularUInt32Field(value: &self.particles10Um) }() - case 10: try { try decoder.decodeSingularUInt32Field(value: &self.particles25Um) }() - case 11: try { try decoder.decodeSingularUInt32Field(value: &self.particles50Um) }() - case 12: try { try decoder.decodeSingularUInt32Field(value: &self.particles100Um) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._pm10Standard) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self._pm25Standard) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self._pm100Standard) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self._pm10Environmental) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self._pm25Environmental) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self._pm100Environmental) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self._particles03Um) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self._particles05Um) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self._particles10Um) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self._particles25Um) }() + case 11: try { try decoder.decodeSingularUInt32Field(value: &self._particles50Um) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self._particles100Um) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.pm10Standard != 0 { - try visitor.visitSingularUInt32Field(value: self.pm10Standard, fieldNumber: 1) - } - if self.pm25Standard != 0 { - try visitor.visitSingularUInt32Field(value: self.pm25Standard, fieldNumber: 2) - } - if self.pm100Standard != 0 { - try visitor.visitSingularUInt32Field(value: self.pm100Standard, fieldNumber: 3) - } - if self.pm10Environmental != 0 { - try visitor.visitSingularUInt32Field(value: self.pm10Environmental, fieldNumber: 4) - } - if self.pm25Environmental != 0 { - try visitor.visitSingularUInt32Field(value: self.pm25Environmental, fieldNumber: 5) - } - if self.pm100Environmental != 0 { - try visitor.visitSingularUInt32Field(value: self.pm100Environmental, fieldNumber: 6) - } - if self.particles03Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles03Um, fieldNumber: 7) - } - if self.particles05Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles05Um, fieldNumber: 8) - } - if self.particles10Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles10Um, fieldNumber: 9) - } - if self.particles25Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles25Um, fieldNumber: 10) - } - if self.particles50Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles50Um, fieldNumber: 11) - } - if self.particles100Um != 0 { - try visitor.visitSingularUInt32Field(value: self.particles100Um, fieldNumber: 12) - } + // 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 + try { if let v = self._pm10Standard { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._pm25Standard { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._pm100Standard { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3) + } }() + try { if let v = self._pm10Environmental { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._pm25Environmental { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) + } }() + try { if let v = self._pm100Environmental { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 6) + } }() + try { if let v = self._particles03Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + } }() + try { if let v = self._particles05Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8) + } }() + try { if let v = self._particles10Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() + try { if let v = self._particles25Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 10) + } }() + try { if let v = self._particles50Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 11) + } }() + try { if let v = self._particles100Um { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 12) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: AirQualityMetrics, rhs: AirQualityMetrics) -> Bool { - if lhs.pm10Standard != rhs.pm10Standard {return false} - if lhs.pm25Standard != rhs.pm25Standard {return false} - if lhs.pm100Standard != rhs.pm100Standard {return false} - if lhs.pm10Environmental != rhs.pm10Environmental {return false} - if lhs.pm25Environmental != rhs.pm25Environmental {return false} - if lhs.pm100Environmental != rhs.pm100Environmental {return false} - if lhs.particles03Um != rhs.particles03Um {return false} - if lhs.particles05Um != rhs.particles05Um {return false} - if lhs.particles10Um != rhs.particles10Um {return false} - if lhs.particles25Um != rhs.particles25Um {return false} - if lhs.particles50Um != rhs.particles50Um {return false} - if lhs.particles100Um != rhs.particles100Um {return false} + if lhs._pm10Standard != rhs._pm10Standard {return false} + if lhs._pm25Standard != rhs._pm25Standard {return false} + if lhs._pm100Standard != rhs._pm100Standard {return false} + if lhs._pm10Environmental != rhs._pm10Environmental {return false} + if lhs._pm25Environmental != rhs._pm25Environmental {return false} + if lhs._pm100Environmental != rhs._pm100Environmental {return false} + if lhs._particles03Um != rhs._particles03Um {return false} + if lhs._particles05Um != rhs._particles05Um {return false} + if lhs._particles10Um != rhs._particles10Um {return false} + if lhs._particles25Um != rhs._particles25Um {return false} + if lhs._particles50Um != rhs._particles50Um {return false} + if lhs._particles100Um != rhs._particles100Um {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LocalStats" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "uptime_seconds"), + 2: .standard(proto: "channel_utilization"), + 3: .standard(proto: "air_util_tx"), + 4: .standard(proto: "num_packets_tx"), + 5: .standard(proto: "num_packets_rx"), + 6: .standard(proto: "num_packets_rx_bad"), + 7: .standard(proto: "num_online_nodes"), + 8: .standard(proto: "num_total_nodes"), + ] + + 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.decodeSingularFloatField(value: &self.channelUtilization) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.airUtilTx) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsTx) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsRx) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsRxBad) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineNodes) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.numTotalNodes) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.uptimeSeconds != 0 { + try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) + } + if self.channelUtilization != 0 { + try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 2) + } + if self.airUtilTx != 0 { + try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 3) + } + if self.numPacketsTx != 0 { + try visitor.visitSingularUInt32Field(value: self.numPacketsTx, fieldNumber: 4) + } + if self.numPacketsRx != 0 { + try visitor.visitSingularUInt32Field(value: self.numPacketsRx, fieldNumber: 5) + } + if self.numPacketsRxBad != 0 { + try visitor.visitSingularUInt32Field(value: self.numPacketsRxBad, fieldNumber: 6) + } + if self.numOnlineNodes != 0 { + try visitor.visitSingularUInt32Field(value: self.numOnlineNodes, fieldNumber: 7) + } + if self.numTotalNodes != 0 { + try visitor.visitSingularUInt32Field(value: self.numTotalNodes, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: LocalStats, rhs: LocalStats) -> Bool { + if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs.channelUtilization != rhs.channelUtilization {return false} + if lhs.airUtilTx != rhs.airUtilTx {return false} + if lhs.numPacketsTx != rhs.numPacketsTx {return false} + if lhs.numPacketsRx != rhs.numPacketsRx {return false} + if lhs.numPacketsRxBad != rhs.numPacketsRxBad {return false} + if lhs.numOnlineNodes != rhs.numOnlineNodes {return false} + if lhs.numTotalNodes != rhs.numTotalNodes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1046,6 +1520,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 3: .standard(proto: "environment_metrics"), 4: .standard(proto: "air_quality_metrics"), 5: .standard(proto: "power_metrics"), + 6: .standard(proto: "local_stats"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1107,6 +1582,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .powerMetrics(v) } }() + case 6: try { + var v: LocalStats? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .localStats(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .localStats(v) + } + }() default: break } } @@ -1137,6 +1625,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .powerMetrics(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 5) }() + case .localStats?: try { + guard case .localStats(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1174,7 +1666,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor.bitPattern != 0 { + if self.calibrationFactor != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 89d0097c..1f41fe0b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem: @unchecked Sendable { +public struct XModem { // 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. @@ -35,7 +35,7 @@ public struct XModem: @unchecked Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { + public enum Control: SwiftProtobuf.Enum { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,23 +79,34 @@ public struct XModem: @unchecked Sendable { } } - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] - } public init() {} } +#if swift(>=4.2) + +extension XModem.Control: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension XModem: @unchecked Sendable {} +extension XModem.Control: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/protobufs b/protobufs index 2fa7d6a4..b6237629 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 2fa7d6a4b702fcd58b54b0d1d6e4b3b85164f649 +Subproject commit b623762940ebdb1887a3b31b86f4d9cdaa7e6ecf From e2f223b30a0b9f9c580fbf5a781c8e18a2581e0e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 19 Aug 2024 21:50:28 -0700 Subject: [PATCH 137/333] Initial validation --- Meshtastic/Views/Helpers/SecureInput.swift | 8 +++- .../Settings/Config/SecurityConfig.swift | 40 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index b4fa54eb..663e847c 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -11,12 +11,14 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String + @Binding private var isValid: Bool @State private var isSecure: Bool = true private var title: String - init(_ title: String, text: Binding) { + init(_ title: String, text: Binding, isValid: Binding) { self.title = title self._text = text + self._isValid = isValid } var body: some View { @@ -41,6 +43,10 @@ struct SecureInput: View { .disableAutocorrection(true) .textSelection(.enabled) .lineLimit(...3) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(isValid ? Color.clear : Color.red, lineWidth: 2.0) + ) } }.padding(.trailing, 36) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index caf64e57..5e6dbb18 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -22,8 +22,11 @@ struct SecurityConfig: View { @State var hasChanges = false @State var publicKey = "" + @State var hasValidPublicKey: Bool = false @State var privateKey = "" + @State var hasValidPrivateKey: Bool = false @State var adminKey = "" + @State var hasValidAdminKey: Bool = true @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false @@ -38,21 +41,29 @@ struct SecurityConfig: View { Section(header: Text("Admin & Direct Message Keys")) { VStack(alignment: .leading) { Label("Public Key", systemImage: "key") - SecureInput("Public Key", text: $publicKey) + SecureInput("Public Key", text: $publicKey, isValid: $hasValidPublicKey) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidPublicKey ? Color.clear : Color.red, lineWidth: 2.0) + ) Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } VStack(alignment: .leading) { Label("Private Key", systemImage: "key.fill") - SecureInput("Private Key", text: $privateKey) + SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidPrivateKey ? Color.clear : Color.red, lineWidth: 2.0) + ) Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } VStack(alignment: .leading) { Label("Admin Key", systemImage: "key.viewfinder") - SecureInput("Private Key", text: $adminKey) + SecureInput("Admin Key", text: $adminKey, isValid: $hasValidAdminKey) Text("The public key authorized to send admin messages to this node.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) @@ -121,12 +132,30 @@ struct SecurityConfig: View { if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } .onChange(of: publicKey) { _ in + let tempKey = Data(base64Encoded: publicKey) ?? Data() + if tempKey.count == 32 { + hasValidPublicKey = true + } else { + hasValidPublicKey = false + } hasChanges = true } .onChange(of: privateKey) { _ in + let tempKey = Data(base64Encoded: privateKey) ?? Data() + if tempKey.count == 32 { + hasValidPrivateKey = true + } else { + hasValidPrivateKey = false + } hasChanges = true } .onChange(of: adminKey) { _ in + let tempAdminKey = Data(base64Encoded: adminKey) ?? Data() + if tempAdminKey.count == 0 || tempAdminKey.count == 32 { + hasValidAdminKey = true + } else { + hasValidAdminKey = false + } hasChanges = true } .onFirstAppear { @@ -140,6 +169,11 @@ struct SecurityConfig: View { } SaveConfigButton(node: node, hasChanges: $hasChanges) { + + if !hasValidAdminKey || !hasValidPrivateKey || !hasValidAdminKey { + return + } + guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), let fromUser = connectedNode.user, let toUser = node?.user else { From b8861c0e0f13664486e8e36c8fa3dcf9125487aa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 Aug 2024 07:33:29 -0700 Subject: [PATCH 138/333] Fix weird validation bug --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 5e6dbb18..e0e501c5 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -149,9 +149,11 @@ struct SecurityConfig: View { } hasChanges = true } - .onChange(of: adminKey) { _ in - let tempAdminKey = Data(base64Encoded: adminKey) ?? Data() - if tempAdminKey.count == 0 || tempAdminKey.count == 32 { + .onChange(of: adminKey) { key in + let tempKey = Data(base64Encoded: key) ?? Data() + if key.isEmpty { + hasValidAdminKey = true + } else if tempKey.count == 32 { hasValidAdminKey = true } else { hasValidAdminKey = false From 7657a2c669d80e6d81bc2e0cd61e25cb1b5eff78 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 Aug 2024 07:37:02 -0700 Subject: [PATCH 139/333] Clean up secure input control --- Meshtastic/Views/Helpers/SecureInput.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index 663e847c..a2994747 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -32,7 +32,6 @@ struct SecureInput: View { .keyboardType(.alphabet) .foregroundStyle(.tertiary) .disableAutocorrection(true) - .textSelection(.enabled) } else { TextField(title, text: $text, axis: .vertical) .font(idiom == .phone ? .caption : .callout) From eb3efd88adf1b72c157f8768aacfcf2d2166c561 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 Aug 2024 07:40:20 -0700 Subject: [PATCH 140/333] fix return statement --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e0e501c5..597324e0 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -172,7 +172,7 @@ struct SecurityConfig: View { SaveConfigButton(node: node, hasChanges: $hasChanges) { - if !hasValidAdminKey || !hasValidPrivateKey || !hasValidAdminKey { + if !hasValidPublicKey || !hasValidPrivateKey || !hasValidAdminKey { return } From 39c31980b5c82011db4917d1f7c24d02b70a07b1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 Aug 2024 18:50:28 -0700 Subject: [PATCH 141/333] Fix channel ack bug --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Messages/ChannelMessageList.swift | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7d5f9023..e267102e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1645,7 +1645,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1680,7 +1680,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1712,7 +1712,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1745,7 +1745,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 95b727f0..c0852a36 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -84,14 +84,16 @@ struct ChannelMessageList: View { } HStack { - if currentUser && message.ackError == 0 { - // Empty Error - Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + if currentUser && message.receivedACK { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .foregroundStyle(ackErrorVal?.color ?? .red) + .foregroundStyle(ackErrorVal?.color ?? Color.red) .font(.caption2) + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting to be acknowledged. . .").font( + .caption2) + .foregroundColor(.orange) } else if isDetectionSensorMessage { let messageDate = message.timestamp Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) From af5f58af8185d510b2d5e3db822776b210159974 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 Aug 2024 20:03:40 -0700 Subject: [PATCH 142/333] Padding and frame for environment widgets --- .../Helpers/Weather/LocalWeatherConditions.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 27675141..612641fe 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -98,8 +98,9 @@ struct WeatherConditionsCompactWidget: View { Label { Text(description) } icon: { Image(systemName: symbolName).symbolRenderingMode(.multicolor) } .font(.caption) Text(temperature) - .font(temperature.length < 4 ? .system(size: 90) : .system(size: 60) ) + .font(temperature.length < 4 ? .system(size: 80) : .system(size: 60) ) } + .padding(.horizontal, 5) .frame(maxWidth: .infinity) .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) @@ -121,7 +122,7 @@ struct HumidityCompactWidget: View { .fixedSize(horizontal: false, vertical: true) .font(.caption) } - .padding(.horizontal) + .padding(.horizontal, 5) .frame(maxWidth: .infinity) .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) @@ -143,7 +144,8 @@ struct PressureCompactWidget: View { Text(unit) } .padding(.horizontal, 5) - .frame(width: 175, height: 175) + .frame(maxWidth: .infinity) + .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -161,8 +163,9 @@ struct WindCompactWidget: View { .font(.system(size: 35)) Text("Gusts \(gust)") } - //.padding(.horizontal) - .frame(width: 175, height: 175) + .padding(.horizontal, 5) + .frame(maxWidth: .infinity) + .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } From aa1874cfd0e89f53635e3abbe3c43a44ddd8d8b1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 20 Aug 2024 20:18:19 -0700 Subject: [PATCH 143/333] Moor padding --- .../Weather/LocalWeatherConditions.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 612641fe..eb93897f 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -100,9 +100,9 @@ struct WeatherConditionsCompactWidget: View { Text(temperature) .font(temperature.length < 4 ? .system(size: 80) : .system(size: 60) ) } - .padding(.horizontal, 5) + .padding(10) .frame(maxWidth: .infinity) - .frame(height: 150) + .frame(height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -122,9 +122,9 @@ struct HumidityCompactWidget: View { .fixedSize(horizontal: false, vertical: true) .font(.caption) } - .padding(.horizontal, 5) + .padding(10) .frame(maxWidth: .infinity) - .frame(height: 150) + .frame(height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -136,16 +136,16 @@ struct PressureCompactWidget: View { var body: some View { VStack(alignment: .leading) { Label { Text("PRESSURE") } icon: { Image(systemName: "gauge").symbolRenderingMode(.multicolor) } - .font(.callout) + .font(.caption) Text(pressure) .font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) ) Text(low ? "LOW" : "HIGH") .padding(.bottom) Text(unit) } - .padding(.horizontal, 5) + .padding(10) .frame(maxWidth: .infinity) - .frame(height: 150) + .frame(height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -158,14 +158,15 @@ struct WindCompactWidget: View { VStack(alignment: .leading) { Label { Text("WIND") } icon: { Image(systemName: "wind").foregroundColor(.accentColor) } Text("\(direction)") + .font(.caption) .padding(.bottom, 10) Text(speed) .font(.system(size: 35)) Text("Gusts \(gust)") } - .padding(.horizontal, 5) + .padding(10) .frame(maxWidth: .infinity) - .frame(height: 150) + .frame(height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } From 72ebe46d38453d0fc0ac25f3cb1c0b8901e2cc2d Mon Sep 17 00:00:00 2001 From: tekstrand Date: Wed, 21 Aug 2024 08:55:48 -0500 Subject: [PATCH 144/333] 885 fix reguires typo -> requires --- Meshtastic/Views/Helpers/Help/LockLegend.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift index 36716798..f8dacd2f 100644 --- a/Meshtastic/Views/Helpers/Help/LockLegend.swift +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -34,7 +34,7 @@ struct LockLegend: View { Text("Public Key Encryption") .fontWeight(.semibold) } - Text("Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater.") + Text("Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater.") .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .font(.callout) .fixedSize(horizontal: false, vertical: true) From eb18b61f032d34237204f7cce30ffbaeba6b5edb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 Aug 2024 14:13:16 -0700 Subject: [PATCH 145/333] Add wind to environment metrics --- .../Weather/LocalWeatherConditions.swift | 8 +++-- .../Views/Nodes/EnvironmentMetricsLog.swift | 6 ++-- .../Views/Nodes/Helpers/NodeDetail.swift | 31 ++++++++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index eb93897f..7dc8e92c 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -158,11 +158,13 @@ struct WindCompactWidget: View { VStack(alignment: .leading) { Label { Text("WIND") } icon: { Image(systemName: "wind").foregroundColor(.accentColor) } Text("\(direction)") - .font(.caption) + .font(gust.isEmpty ? .callout : .caption) .padding(.bottom, 10) Text(speed) - .font(.system(size: 35)) - Text("Gusts \(gust)") + .font(gust.isEmpty ? .system(size: 45) : .system(size: 35)) + if !gust.isEmpty { + Text("Gusts \(gust)") + } } .padding(10) .frame(maxWidth: .infinity) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 571511cf..0cd35248 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -94,10 +94,12 @@ struct EnvironmentMetricsLog: View { } } TableColumn("Wind Speed") { em in - Text("\(String(format: "%.1f", em.windSpeed)) hPa") + let windSpeed = Measurement(value: Double(em.windSpeed), unit: UnitSpeed.kilometersPerHour) + Text(windSpeed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0))))) } TableColumn("Wind Direction") { em in - Text("\(String(format: "%.1f", em.windDirection)) hPa") + let direction = cardinalValue(from: Double(em.windDirection)) + Text(direction) } TableColumn("timestamp") { em in Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 8a78c08c..6724e805 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -194,7 +194,11 @@ struct NodeDetail: View { PressureCompactWidget(pressure: String(format: "%.2f", node.latestEnvironmentMetrics?.barometricPressure ?? 0.0), unit: "hPA", low: node.latestEnvironmentMetrics?.barometricPressure ?? 0.0 <= 1009.144) } if node.latestEnvironmentMetrics?.windSpeed ?? 0.0 > 0.0 { - WindCompactWidget(speed: String(node.latestEnvironmentMetrics?.windSpeed ?? 0.0), gust: String(node.latestEnvironmentMetrics?.windGust ?? 0.0), direction: "") + let windSpeed = Measurement(value: Double(node.latestEnvironmentMetrics?.windSpeed ?? 0.0), unit: UnitSpeed.metersPerSecond) + let windGust = Measurement(value: Double(node.latestEnvironmentMetrics?.windGust ?? 0.0), unit: UnitSpeed.metersPerSecond) + let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) + WindCompactWidget(speed: windSpeed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), + gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) } } .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) @@ -411,3 +415,28 @@ struct NodeDetail: View { } } } + +func cardinalValue(from heading: Double) -> String { + switch heading { + case 0 ..< 22.5: + return "North" + case 22.5 ..< 67.5: + return "North East" + case 67.5 ..< 112.5: + return "East" + case 112.5 ..< 157.5: + return "South East" + case 157.5 ..< 202.5: + return "South" + case 202.5 ..< 247.5: + return "South West" + case 247.5 ..< 292.5: + return "West" + case 292.5 ..< 337.5: + return "North West" + case 337.5 ... 360.0: + return "North" + default: + return "" + } +} From 731036ccab0372d07e83a8175213e0b58c1e68b2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 Aug 2024 15:37:04 -0700 Subject: [PATCH 146/333] Fix up weather widget frames --- .../Weather/LocalWeatherConditions.swift | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 7dc8e92c..0f0ce860 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -100,9 +100,8 @@ struct WeatherConditionsCompactWidget: View { Text(temperature) .font(temperature.length < 4 ? .system(size: 80) : .system(size: 60) ) } - .padding(10) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -122,9 +121,8 @@ struct HumidityCompactWidget: View { .fixedSize(horizontal: false, vertical: true) .font(.caption) } - .padding(10) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -143,9 +141,8 @@ struct PressureCompactWidget: View { .padding(.bottom) Text(unit) } - .padding(10) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -166,9 +163,8 @@ struct WindCompactWidget: View { Text("Gusts \(gust)") } } - .padding(10) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } From 40be462262e53a19d88b642c9b42d9e08c9080b9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 Aug 2024 15:37:47 -0700 Subject: [PATCH 147/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e267102e..bc7b5707 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1645,7 +1645,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1680,7 +1680,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1706,13 +1706,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1739,13 +1739,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From cecdd77a059184f9d26d08f7c20cec1acdabe5b1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 21 Aug 2024 16:59:36 -0700 Subject: [PATCH 148/333] Fix security config update upsert method to include admin bool --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6f2520ee..93ba6493 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -818,6 +818,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s newSecurityConfig.serialEnabled = config.serialEnabled newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled fetchedNode[0].securityConfig = newSecurityConfig } else { fetchedNode[0].securityConfig?.publicKey = config.publicKey @@ -827,6 +828,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled } if sessionPasskey?.count != 0 { fetchedNode[0].sessionPasskey = sessionPasskey From 948d40e82585f79c810f21ca379d1d2d2b321017 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 19:12:07 -0700 Subject: [PATCH 149/333] Update weather --- .../Weather/LocalWeatherConditions.swift | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 0f0ce860..3ea1dee3 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -95,12 +95,20 @@ struct WeatherConditionsCompactWidget: View { let description: String var body: some View { VStack(alignment: .leading) { - Label { Text(description) } icon: { Image(systemName: symbolName).symbolRenderingMode(.multicolor) } - .font(.caption) + HStack(spacing: 5.0) { + Image(systemName: symbolName) + .foregroundColor(.accentColor) + .font(.callout) + Text(description) + .lineLimit(2) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .fixedSize(horizontal: false, vertical: true) + .font(.caption) + } Text(temperature) - .font(temperature.length < 4 ? .system(size: 80) : .system(size: 60) ) + .font(temperature.length < 4 ? .system(size: 76) : .system(size: 60) ) } - .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } @@ -111,17 +119,23 @@ struct HumidityCompactWidget: View { let dewPoint: String var body: some View { VStack(alignment: .leading) { - Label { Text("HUMIDITY") } icon: { Image(systemName: "humidity").symbolRenderingMode(.multicolor) } - .font(.caption) + HStack(spacing: 5.0) { + Image(systemName: "humidity") + .foregroundColor(.accentColor) + .font(.callout) + Text("HUMIDITY") + .font(.caption) + } Text("\(humidity)%") .font(.largeTitle) - .padding(.bottom) + .padding(.bottom, 5) Text("The dew point is \(dewPoint) right now.") .lineLimit(3) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .fixedSize(horizontal: false, vertical: true) - .font(.caption) + .font(.caption2) } - .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } @@ -133,15 +147,20 @@ struct PressureCompactWidget: View { let low: Bool var body: some View { VStack(alignment: .leading) { - Label { Text("PRESSURE") } icon: { Image(systemName: "gauge").symbolRenderingMode(.multicolor) } - .font(.caption) + HStack(spacing: 5.0) { + Image(systemName: "gauge") + .foregroundColor(.accentColor) + .font(.callout) + Text("PRESSURE") + .font(.caption) + } Text(pressure) .font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) ) Text(low ? "LOW" : "HIGH") - .padding(.bottom) + .padding(.bottom, 10) Text(unit) } - .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } @@ -163,7 +182,7 @@ struct WindCompactWidget: View { Text("Gusts \(gust)") } } - .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 130, idealHeight: 140, maxHeight: 150) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } From f7c26f896f42fd66671e03a72dccc3be17a1b2a9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 19:28:13 -0700 Subject: [PATCH 150/333] New scroll methods --- Meshtastic/Views/Messages/ChannelMessageList.swift | 10 +++++----- Meshtastic/Views/Messages/UserMessageList.swift | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index c0852a36..74a4538a 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -128,14 +128,14 @@ struct ChannelMessageList: View { } .padding([.top]) .scrollDismissesKeyboard(.immediately) - .onAppear { - if channel.allPrivateMessages.count > 0 { - scrollView.scrollTo(channel.allPrivateMessages.last!.messageId) + .onFirstAppear { + withAnimation { + scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } } .onChange(of: channel.allPrivateMessages, perform: { _ in - if channel.allPrivateMessages.count > 0 { - scrollView.scrollTo(channel.allPrivateMessages.last!.messageId) + withAnimation { + scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } }) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index b0ec3a10..a53f788d 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -116,14 +116,14 @@ struct UserMessageList: View { } .padding([.top]) .scrollDismissesKeyboard(.immediately) - .onAppear { - if user.messageList.count > 0 { - scrollView.scrollTo(user.messageList.last!.messageId) + .onFirstAppear { + withAnimation { + scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } } .onChange(of: user.messageList, perform: { _ in - if user.messageList.count > 0 { - scrollView.scrollTo(user.messageList.last!.messageId) + withAnimation { + scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } }) } From 55c90a815ccb38dd6b893b712778c566c6cad91c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 19:49:06 -0700 Subject: [PATCH 151/333] Add some text for the new admin channel settings, show missing ack text --- Localizable.xcstrings | 6 ++++++ Meshtastic/Views/Messages/ChannelMessageList.swift | 6 +++++- Meshtastic/Views/Settings/AppSettings.swift | 3 +++ Meshtastic/Views/Settings/Channels.swift | 2 +- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 3 ++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index c518a26f..debce7c6 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -16404,6 +16404,9 @@ }, "Pin B" : { + }, + "PKI based node administration, requires firmware version 2.5+" : { + }, "Please connect to a radio to configure settings." : { @@ -18889,6 +18892,9 @@ }, "Security Config" : { + }, + "Security Config Settings require a firmware version 2.5+" : { + }, "Select a channel" : { diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 74a4538a..5e0bcd04 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -94,7 +94,11 @@ struct ChannelMessageList: View { Text("Waiting to be acknowledged. . .").font( .caption2) .foregroundColor(.orange) - } else if isDetectionSensorMessage { + } else if !isDetectionSensorMessage { + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) + } else { let messageDate = message.timestamp Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index ef3bc384..02286655 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -28,6 +28,9 @@ struct AppSettings: View { Label("Administration", systemImage: "gearshape.2") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("PKI based node administration, requires firmware version 2.5+") + .foregroundStyle(.secondary) + .font(.caption) } Section(header: Text("environment")) { VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index bec86958..ce48b658 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -146,7 +146,7 @@ struct Channels: View { ChannelForm(channelIndex: $channelIndex, channelName: $channelName, channelKeySize: $channelKeySize, channelKey: $channelKey, channelRole: $channelRole, uplink: $uplink, downlink: $downlink, positionPrecision: $positionPrecision, preciseLocation: $preciseLocation, positionsEnabled: $positionsEnabled, hasChanges: $hasChanges, hasValidKey: $hasValidKey, supportedVersion: $supportedVersion) .presentationDetents([.large]) .presentationDragIndicator(.visible) - .onAppear { + .onFirstAppear { supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame } HStack { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 597324e0..e07ed945 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -37,7 +37,8 @@ struct SecurityConfig: View { VStack { Form { ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues) - + Text("Security Config Settings require a firmware version 2.5+") + .font(.title3) Section(header: Text("Admin & Direct Message Keys")) { VStack(alignment: .leading) { Label("Public Key", systemImage: "key") From fa872a9d221b4969827091279e5d22918e9cc1c3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 20:10:57 -0700 Subject: [PATCH 152/333] Assorted cleanup --- Meshtastic/Views/Messages/UserList.swift | 2 +- .../Nodes/Helpers/Map/PositionPopover.swift | 4 +++- .../Views/Nodes/Helpers/NodeListFilter.swift | 20 +++++++++---------- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index cb3582ab..bf953d5b 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -227,7 +227,7 @@ struct UserList: View { .onChange(of: maxDistance) { _ in searchUserList() } - .onFirstAppear { + .onAppear { searchUserList() } .safeAreaInset(edge: .bottom, alignment: .leading) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 6a717629..3f0a528b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -232,7 +232,9 @@ struct PositionPopover: View { } BatteryGauge(node: position.nodePosition!) } - LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) + if position.nodePosition?.hopsAway ?? 0 == 0 && !(position.nodePosition?.viaMqtt ?? false) { + LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) + } Spacer() } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 66d88b28..210e58d1 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -91,18 +91,18 @@ struct NodeListFilter: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) - Toggle(isOn: $isEnvironment) { - - Label { - Text("Environment") - } icon: { - Image(systemName: "cloud.sun") - .symbolRenderingMode(.multicolor) + if filterTitle == "Node Filters" { + Toggle(isOn: $isEnvironment) { + Label { + Text("Environment") + } icon: { + Image(systemName: "cloud.sun") + .symbolRenderingMode(.multicolor) + } } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - Toggle(isOn: $distanceFilter) { Label { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index e252aa95..67636328 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -344,7 +344,7 @@ struct NodeList: View { self.selectedNode = nil } } - .onFirstAppear { + .onAppear { Task { await searchNodeList() } From 05ea8e658649da190101803db41921efed771bee Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 20:19:24 -0700 Subject: [PATCH 153/333] Remove saving borked position as a node info --- Meshtastic/Persistence/UpdateCoreData.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6f2520ee..ba96ec9a 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -384,12 +384,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } } } else { - - if (try? NodeInfo(serializedData: packet.decoded.payload)) != nil { - upsertNodeInfoPacket(packet: packet, context: context) - } else { - Logger.data.error("💥 Empty POSITION_APP Packet: \((try? packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") - } + Logger.data.error("💥 Empty POSITION_APP Packet: \((try? packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } } } catch { From 53d15a4a33d13126e56ef530c507368d2ffb465f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 20:25:58 -0700 Subject: [PATCH 154/333] Dont show acks for received messages --- Meshtastic/Views/Messages/ChannelMessageList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 5e0bcd04..91a922c9 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -94,7 +94,7 @@ struct ChannelMessageList: View { Text("Waiting to be acknowledged. . .").font( .caption2) .foregroundColor(.orange) - } else if !isDetectionSensorMessage { + } else if currentUser && !isDetectionSensorMessage { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) .foregroundStyle(ackErrorVal?.color ?? Color.red) .font(.caption2) From 1253f84cf370cf3571cc0da1570b4dc4809b7651 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 23 Aug 2024 20:40:35 -0700 Subject: [PATCH 155/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e267102e..1d2c8a20 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1645,7 +1645,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1680,7 +1680,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1712,7 +1712,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1745,7 +1745,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.1; + MARKETING_VERSION = 2.5.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 85f9cc8ad3eed0d43a14c391a17a42fbdf060f4f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Aug 2024 18:44:08 -0700 Subject: [PATCH 156/333] Upcate live activity to use the new local stats protobuf --- Localizable.xcstrings | 15 +- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/MeshPackets.swift | 77 +-- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 475 ++++++++++++++++++ Meshtastic/Views/Bluetooth/Connect.swift | 20 +- Widgets/MeshActivityAttributes.swift | 12 +- Widgets/WidgetsLiveActivity.swift | 118 ++--- 8 files changed, 601 insertions(+), 122 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents diff --git a/Localizable.xcstrings b/Localizable.xcstrings index f1806ddf..9989f2b7 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6325,7 +6325,7 @@ "Direct Message Help" : { }, - "Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater." : { + "Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : { }, "Direct messages are using the shared key for the channel." : { @@ -7227,12 +7227,12 @@ }, "Favorites" : { - }, - "Fetch the latest position of a cetain node" : { - }, "Favorites and nodes with recent messages show up at the top of the contact list." : { - + + }, + "Fetch the latest position of a cetain node" : { + }, "Fifteen Minutes" : { @@ -14565,9 +14565,10 @@ }, "Message content exceeds 228 bytes." : { + }, "Message Status Options" : { - + }, "message.details" : { "localizations" : { @@ -22241,7 +22242,7 @@ } } }, - "Updated Device Metrics Data." : { + "Updated Node Stats Data." : { }, "Updated: %@" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6b02e8f1..dcb2a600 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -368,6 +368,7 @@ DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = ""; }; DD77093E2AA1B146007A8BF0 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = ""; }; + DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = ""; }; DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = ""; }; DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = ""; }; DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = ""; }; @@ -1881,6 +1882,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */, @@ -1924,7 +1926,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */; + currentVersion = DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d3bef97a..12ed1f1b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -681,14 +681,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { - // Only log telemetry from the mesh not the connected device - if connectedNode != Int64(packet.from) { - let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) - MeshLogger.log("📈 \(logString)") - } else { - // If it is the connected node - } - if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { + let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) + MeshLogger.log("📈 \(logString)") + + if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { /// Other unhandled telemetry packets return } @@ -727,6 +723,18 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.windLull = telemetryMessage.environmentMetrics.windLull telemetry.windDirection = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection) telemetry.metricsType = 1 + } else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { + // Local Stats for Live activity + telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds) + telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization + telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx + telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx) + telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx) + telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) + telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) + telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) + telemetry.metricsType = 6 + Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)") } telemetry.snr = packet.rxSnr telemetry.rssi = packet.rxRssi @@ -743,34 +751,45 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } try context.save() - // Only log telemetry from the mesh not the connected device - if connectedNode != Int64(packet.from) { - Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())") - } else if telemetry.metricsType == 0 { + + Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())") + if telemetry.metricsType == 0 { // Connected Device Metrics // ------------------------ // Low Battery notification - if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(UUID().uuidString)"), - title: "Critically Low Battery!", - subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", - content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", - target: "nodes", - path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" - ) - ] - manager.schedule() + if connectedNode != Int64(packet.from) { + if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(UUID().uuidString)"), + title: "Critically Low Battery!", + subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", + content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", + target: "nodes", + path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" + ) + ] + manager.schedule() + } } + } else if telemetry.metricsType == 6 { // Update our live activity if there is one running, not available on mac iOS >= 16.2 #if !targetEnvironment(macCatalyst) - let oneMinuteLater = Calendar.current.date(byAdding: .minute, value: (Int(1) ), to: Date())! - let date = Date.now...oneMinuteLater - let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(timerRange: date, connected: true, channelUtilization: telemetry.channelUtilization, airtime: telemetry.airUtilTx, batteryLevel: UInt32(telemetry.batteryLevel), nodes: 17, nodesOnline: 9) - let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Device Metrics Data.", sound: .default) + let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! + let date = Date.now...fifteenMinutesLater + let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: UInt32(telemetry.uptimeSeconds), + channelUtilization: telemetry.channelUtilization, + airtime: telemetry.airUtilTx, + sentPackets: UInt32(telemetry.numPacketsTx), + receivedPackets: UInt32(telemetry.numPacketsRx), + badReceivedPackets: UInt32(telemetry.numPacketsRxBad), + nodesOnline: UInt32(telemetry.numOnlineNodes), + totalNodes: UInt32(telemetry.numTotalNodes), + timerRange: date) + + let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default) let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil) let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 3ddf90f8..04d3cc1a 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 42.xcdatamodel + MeshtasticDataModelV 43.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents new file mode 100644 index 00000000..544d44c1 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 43.xcdatamodel/contents @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 0048e430..58d8757b 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -327,17 +327,25 @@ struct Connect: View { #if canImport(ActivityKit) func startNodeActivity() { liveActivityStarted = true - let timerSeconds = 60 - let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + // 15 Minutes Local Stats Interval + let timerSeconds = 900 + let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")) + let mostRecent = localStats?.lastObject as? TelemetryEntity let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown") let future = Date(timeIntervalSinceNow: Double(timerSeconds)) + let initialContentState = MeshActivityAttributes.ContentState(uptimeSeconds: UInt32(mostRecent?.uptimeSeconds ?? 0), + channelUtilization: mostRecent?.channelUtilization ?? 0.0, + airtime: mostRecent?.airUtilTx ?? 0.0, + sentPackets: UInt32(mostRecent?.numPacketsTx ?? 0), + receivedPackets: UInt32(mostRecent?.numPacketsRx ?? 0), + badReceivedPackets: UInt32(mostRecent?.numPacketsRxBad ?? 0), + nodesOnline: UInt32(mostRecent?.numOnlineNodes ?? 0), + totalNodes: UInt32(mostRecent?.numTotalNodes ?? 0), + timerRange: Date.now...future) - let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0), nodes: 17, nodesOnline: 9) - - let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!) + let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!) do { let myActivity = try Activity.request(attributes: activityAttributes, content: activityContent, diff --git a/Widgets/MeshActivityAttributes.swift b/Widgets/MeshActivityAttributes.swift index 916377c9..a2abdbba 100644 --- a/Widgets/MeshActivityAttributes.swift +++ b/Widgets/MeshActivityAttributes.swift @@ -15,13 +15,15 @@ struct MeshActivityAttributes: ActivityAttributes { public typealias MeshActivityStatus = ContentState public struct ContentState: Codable, Hashable { // Dynamic stateful properties about your activity go here! - var timerRange: ClosedRange - var connected: Bool + var uptimeSeconds: UInt32 var channelUtilization: Float var airtime: Float - var batteryLevel: UInt32 - var nodes: Int - var nodesOnline: Int + var sentPackets: UInt32 + var receivedPackets: UInt32 + var badReceivedPackets: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 + var timerRange: ClosedRange } // Fixed non-changing properties about your activity go here! diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 396aaac9..690fc298 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -13,7 +13,16 @@ struct WidgetsLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: MeshActivityAttributes.self) { context in - LiveActivityView(nodeName: context.attributes.name, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel, nodes: 17, nodesOnline: 7, timerRange: context.state.timerRange) + LiveActivityView(nodeName: context.attributes.name, + uptimeSeconds: 0, // context.attributes.uptimeSeconds, + channelUtilization: context.state.channelUtilization, + airtime: context.state.airtime, + sentPackets: context.state.sentPackets, + receivedPackets: context.state.receivedPackets, + badReceivedPackets: context.state.badReceivedPackets, + nodesOnline: context.state.nodesOnline, + totalNodes: context.state.totalNodes, + timerRange: context.state.timerRange) .widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)")) } dynamicIsland: { context in @@ -38,24 +47,7 @@ struct WidgetsLiveActivity: Widget { Spacer() } DynamicIslandExpandedRegion(.center) { - VStack(alignment: .center, spacing: 0) { - BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor) - if context.state.batteryLevel == 0 { - Text("< 1%") - .font(.title3) - .foregroundColor(.gray) - .fixedSize() - } else if context.state.batteryLevel < 101 { - Text(String(context.state.batteryLevel) + "%") - .font(.title3) - .foregroundColor(.gray) - .fixedSize() - } else { - Text("PWD") - .font(.title3) - .foregroundColor(.gray) - } - } + // Used to be battery } DynamicIslandExpandedRegion(.trailing, priority: 1) { TimerView(timerRange: context.state.timerRange) @@ -100,38 +92,40 @@ struct WidgetsLiveActivity: Widget { } } -struct WidgetsLiveActivity_Previews: PreviewProvider { - static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") - static let state = MeshActivityAttributes.ContentState( - timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) - - static var previews: some View { - attributes - .previewContext(state, viewKind: .dynamicIsland(.compact)) - .previewDisplayName("Compact") - attributes - .previewContext(state, viewKind: .dynamicIsland(.minimal)) - .previewDisplayName("Minimal") - attributes - .previewContext(state, viewKind: .dynamicIsland(.expanded)) - .previewDisplayName("Expanded") - attributes - .previewContext(state, viewKind: .content) - .previewDisplayName("Notification") - } -} +//struct WidgetsLiveActivity_Previews: PreviewProvider { +// static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") +// static let state = MeshActivityAttributes.ContentState( +// timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) +// +// static var previews: some View { +// attributes +// .previewContext(state, viewKind: .dynamicIsland(.compact)) +// .previewDisplayName("Compact") +// attributes +// .previewContext(state, viewKind: .dynamicIsland(.minimal)) +// .previewDisplayName("Minimal") +// attributes +// .previewContext(state, viewKind: .dynamicIsland(.expanded)) +// .previewDisplayName("Expanded") +// attributes +// .previewContext(state, viewKind: .content) +// .previewDisplayName("Notification") +// } +//} struct LiveActivityView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.isLuminanceReduced) var isLuminanceReduced var nodeName: String - // var connected: Bool + var uptimeSeconds: UInt32 var channelUtilization: Float var airtime: Float - var batteryLevel: UInt32 - var nodes: Int - var nodesOnline: Int + var sentPackets: UInt32 + var receivedPackets: UInt32 + var badReceivedPackets: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 var timerRange: ClosedRange var body: some View { @@ -143,33 +137,8 @@ struct LiveActivityView: View { .aspectRatio(contentMode: .fit) .frame(width: 65) Spacer() - NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel, nodes: nodes, nodesOnline: nodesOnline) + NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) Spacer() - VStack { - BatteryIcon(batteryLevel: Int32(batteryLevel), font: .title, color: .secondary) - if batteryLevel == 0 { - Text("< 1%") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } else if batteryLevel < 101 { - Text(String(batteryLevel) + "%") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } else { - Text("Plugged In") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } - } } .tint(.primary) .padding([.leading, .top, .bottom]) @@ -183,12 +152,15 @@ struct NodeInfoView: View { @Environment(\.isLuminanceReduced) var isLuminanceReduced var nodeName: String - var timerRange: ClosedRange + var uptimeSeconds: UInt32 var channelUtilization: Float var airtime: Float - var batteryLevel: UInt32 - var nodes: Int - var nodesOnline: Int + var sentPackets: UInt32 + var receivedPackets: UInt32 + var badReceivedPackets: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 + var timerRange: ClosedRange var body: some View { VStack(alignment: .leading, spacing: 0) { From 716c09cf94101d8bd81f2530a94425855bac11c0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Aug 2024 19:26:50 -0700 Subject: [PATCH 157/333] Update live activity to use new local stats packet --- Widgets/WidgetsLiveActivity.swift | 37 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 690fc298..95a1bc94 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -130,19 +130,20 @@ struct LiveActivityView: View { var body: some View { HStack { + Spacer() Image(colorScheme == .light ? "m-logo-black" : "m-logo-white") .resizable() .clipShape(ContainerRelativeShape()) .opacity(isLuminanceReduced ? 0.5 : 1.0) .aspectRatio(contentMode: .fit) - .frame(width: 65) + .frame(minWidth: 25, idealWidth: 45, maxWidth: 55) Spacer() NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) Spacer() } .tint(.primary) .padding([.leading, .top, .bottom]) - .padding(.trailing, 32) + .padding(.trailing, 25) .activityBackgroundTint(colorScheme == .light ? Color("LiveActivityBackground") : Color("AccentColorDimmed")) .activitySystemActionForegroundColor(.primary) } @@ -168,24 +169,36 @@ struct NodeInfoView: View { .font(nodeName.count > 14 ? .callout : .title3) .fontWeight(.semibold) .foregroundStyle(.tint) - Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%") - .font(.headline) + Text("\(String(format: "Ch. Util: %.2f", channelUtilization))% \(String(format: "Airtime: %.2f", airtime))%") + .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("\(String(format: "Airtime: %.2f", airtime))%") - .font(.headline) + Text("Packets Sent \(sentPackets)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text("Packets Received \(receivedPackets)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text("Bad Packets Received \(badReceivedPackets)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text("\(String(format: "Connected: %d nodes online", nodesOnline, totalNodes))") + .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() -// Text("\(String(format: "Connected: %d of %d online", nodesOnline, nodes))") -// .font(.callout) -// .fontWeight(.medium) -// .foregroundStyle(.secondary) -// .opacity(isLuminanceReduced ? 0.8 : 1.0) -// .fixedSize() let now = Date() Text("Last Heard: \(now.formatted())") .font(.caption) From 8ac18430e2e222ca3554ba4b50687722cc2b99a6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Aug 2024 20:56:40 -0700 Subject: [PATCH 158/333] Dynamic island --- Widgets/WidgetsLiveActivity.swift | 63 +++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 95a1bc94..5335db7c 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -28,41 +28,62 @@ struct WidgetsLiveActivity: Widget { } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - Text("Network") - .font(.headline) - .fontWeight(.bold) + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Spacer() + Text("Mesh") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.primary) + .padding(.bottom, 10) + .fixedSize() + Spacer() + } + Text("\(context.state.nodesOnline) online") + .font(.caption) .foregroundStyle(.secondary) .fixedSize() - .padding(.top, 10) Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") - .font(.headline) - .fontWeight(.medium) + .font(.caption) .foregroundStyle(.secondary) .fixedSize() Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") - .font(.headline) - .fontWeight(.medium) + .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Spacer() } DynamicIslandExpandedRegion(.center) { - // Used to be battery - } - DynamicIslandExpandedRegion(.trailing, priority: 1) { TimerView(timerRange: context.state.timerRange) .tint(Color("LightIndigo")) - + } + DynamicIslandExpandedRegion(.trailing, priority: 1) { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Spacer() + Text("Packets") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.primary) + .padding(.bottom, 10) + .fixedSize() + Spacer() + } + Text("Sent \(context.state.sentPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Received \(context.state.receivedPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Bad \(context.state.badReceivedPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() } DynamicIslandExpandedRegion(.bottom) { - Text(context.attributes.name) - .font(context.attributes.name.count > 14 ? .callout : .title3) - .fontWeight(.semibold) - .foregroundStyle(.tint) Text("Last Heard: \(Date().formatted())") .font(.caption) .fontWeight(.medium) - .foregroundStyle(.secondary) + .foregroundStyle(.tint) .fixedSize() } @@ -240,8 +261,9 @@ struct TimerView: View { var body: some View { VStack(alignment: .center) { - Text("NEXT UPDATE") - .font(.caption) + Text("UPDATE IN") + .font(.caption2) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.5 : 1.0) @@ -257,6 +279,7 @@ struct TimerView: View { .foregroundStyle(.secondary) .frame(width: 30, height: 30) .opacity(isLuminanceReduced ? 0.5 : 1.0) + .offset(y: -5) } } } From 799f06f87333e142b1e7b3ddf014cd64f560148a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 24 Aug 2024 21:54:21 -0700 Subject: [PATCH 159/333] Multicolor timer icon --- Widgets/WidgetsLiveActivity.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 5335db7c..f667d3af 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -275,6 +275,7 @@ struct TimerView: View { .fontWeight(.semibold) .foregroundStyle(.tint) Image(systemName: "timer") + .symbolRenderingMode(.multicolor) .resizable() .foregroundStyle(.secondary) .frame(width: 30, height: 30) From b9d7c1588322fb9178ad4be6166a059cd7451c08 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 Aug 2024 09:08:49 -0700 Subject: [PATCH 160/333] Add dupes to bad labels to make it clear these are not all errors --- Widgets/WidgetsLiveActivity.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index f667d3af..0235f566 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -38,7 +38,7 @@ struct WidgetsLiveActivity: Widget { .fixedSize() Spacer() } - Text("\(context.state.nodesOnline) online") + Text("\(context.state.nodesOnline) nodes online") .font(.caption) .foregroundStyle(.secondary) .fixedSize() @@ -66,15 +66,15 @@ struct WidgetsLiveActivity: Widget { .fixedSize() Spacer() } - Text("Sent \(context.state.sentPackets)") + Text("Sent: \(context.state.sentPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Text("Received \(context.state.receivedPackets)") + Text("Received: \(context.state.receivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Text("Bad \(context.state.badReceivedPackets)") + Text("Dupe / Bad: \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() @@ -196,19 +196,19 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Packets Sent \(sentPackets)") + Text("Packets Sent: \(sentPackets)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Packets Received \(receivedPackets)") + Text("Packets Received: \(receivedPackets)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Bad Packets Received \(badReceivedPackets)") + Text("Dupe / Bad Packets: \(badReceivedPackets)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) From 4361a082c5583a15445b145632755323f24c5cee Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 Aug 2024 09:50:01 -0700 Subject: [PATCH 161/333] Sync up online timeframes with the stats at seen in the last 2 hours --- Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift | 4 ++-- Meshtastic/Views/Messages/UserList.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 07ee9117..c1bd5bec 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -52,8 +52,8 @@ extension NodeInfoEntity { } var isOnline: Bool { - let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date()) - if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending { + let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) + if lastHeard?.compare(twoHoursAgo!) == .orderedDescending { return true } return false diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index bf953d5b..3a2c4a47 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -307,7 +307,7 @@ struct UserList: View { } /// Online if isOnline { - let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) + let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } /// Encrypted diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 67636328..c5c92418 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -390,7 +390,7 @@ struct NodeList: View { } /// Online if isOnline { - let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) + let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } /// Encrypted From 25f8e424ed7b9a49b706c7c5b43b2f339a8f542d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 Aug 2024 10:08:32 -0700 Subject: [PATCH 162/333] Sync up isonline with the firmware add local stats extension --- Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift | 4 ++++ Meshtastic/Views/Messages/UserList.swift | 3 +++ Meshtastic/Views/Nodes/MeshMap.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index c1bd5bec..ba8e38fa 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -22,6 +22,10 @@ extension NodeInfoEntity { return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity } + var latestLocalStats: TelemetryEntity? { + return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity + } + var hasPositions: Bool { return positions?.count ?? 0 > 0 } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 3a2c4a47..7ba2205c 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -227,6 +227,9 @@ struct UserList: View { .onChange(of: maxDistance) { _ in searchUserList() } + .onReceive(users.publisher) { _ in + searchUserList() + } .onAppear { searchUserList() } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 9e09a0c0..f471caaf 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -126,7 +126,7 @@ struct MeshMap: View { guard case .map(let selectedNodeNum) = router.navigationState else { return } // TODO: handle deep link for waypoints } - .onChange(of: (selectedMapLayer)) { newMapLayer in + .onChange(of: selectedMapLayer) { newMapLayer in switch selectedMapLayer { case .standard: UserDefaults.mapLayer = newMapLayer diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c5c92418..c0f16dd8 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -344,6 +344,11 @@ struct NodeList: View { self.selectedNode = nil } } + .onReceive(nodes.publisher) { _ in + Task { + await searchNodeList() + } + } .onAppear { Task { await searchNodeList() From cfdcc5a2c3e97c74c0a2f4f96959afbee1b73377 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:14:25 -0700 Subject: [PATCH 163/333] Added Shortcuts provider and Shut Down, Reboot, and Factory Reset Intents. Also added logging for intent errors. --- Localizable.xcstrings | 47 +++++++++++++++---- Meshtastic.xcodeproj/project.pbxproj | 16 +++++++ Meshtastic/AppIntents/AppIntentErrors.swift | 9 +++- .../AppIntents/FactoryResetNodeIntent.swift | 41 ++++++++++++++++ .../AppIntents/MessageChannelIntent.swift | 2 +- .../AppIntents/NodePositionIntent.swift | 1 + Meshtastic/AppIntents/RestartNodeIntent.swift | 42 +++++++++++++++++ .../AppIntents/SendWaypointIntent.swift | 2 +- Meshtastic/AppIntents/ShortcutsProvider.swift | 37 +++++++++++++++ .../AppIntents/ShutDownNodeIntent.swift | 41 ++++++++++++++++ 10 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 Meshtastic/AppIntents/FactoryResetNodeIntent.swift create mode 100644 Meshtastic/AppIntents/RestartNodeIntent.swift create mode 100644 Meshtastic/AppIntents/ShortcutsProvider.swift create mode 100644 Meshtastic/AppIntents/ShutDownNodeIntent.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index f1806ddf..74281fbd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1166,6 +1166,9 @@ }, "Are you sure you want to delete this message?" : { + }, + "Are you sure you want to factory reset the node?" : { + }, "are.you.sure" : { "localizations" : { @@ -6325,7 +6328,7 @@ "Direct Message Help" : { }, - "Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater." : { + "Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : { }, "Direct messages are using the shared key for the channel." : { @@ -7209,6 +7212,9 @@ }, "Factory Reset" : { + }, + "Factory Reset Node" : { + }, "Factory reset your device and app? " : { @@ -7227,12 +7233,12 @@ }, "Favorites" : { - }, - "Fetch the latest position of a cetain node" : { - }, "Favorites and nodes with recent messages show up at the top of the contact list." : { - + + }, + "Fetch the latest position of a cetain node" : { + }, "Fifteen Minutes" : { @@ -14563,11 +14569,15 @@ }, "Message" : { + }, + "Message Channel" : { + }, "Message content exceeds 228 bytes." : { + }, "Message Status Options" : { - + }, "message.details" : { "localizations" : { @@ -16319,6 +16329,9 @@ } } } + }, + "Perform a factory reset on the node you are connected to" : { + }, "phone.gps" : { "localizations" : { @@ -17024,6 +17037,9 @@ } } } + }, + "Reboot Node?" : { + }, "reboot.node" : { "localizations" : { @@ -17389,6 +17405,12 @@ }, "Reset NodeDB" : { + }, + "Restart Node" : { + + }, + "Restart to the node you are connected to" : { + }, "restore" : { @@ -19131,13 +19153,16 @@ "Send" : { }, - "Send a channel message" : { + "Send a Channel Message" : { }, "Send a message to a certain meshtastic channel" : { }, - "Send a waypoint" : { + "Send a shutdown to the node you are connected to" : { + + }, + "Send a Waypoint" : { }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { @@ -19876,6 +19901,12 @@ }, "Show Weather" : { + }, + "Shut Down Node" : { + + }, + "Shut Down Node?" : { + }, "Shutdown Node?" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6b02e8f1..09d0f827 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -36,6 +36,10 @@ BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; + BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; + BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; }; + BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; }; + BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; }; D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; }; @@ -275,6 +279,10 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; + BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; + BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; + BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = ""; }; + BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactoryResetNodeIntent.swift; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -573,6 +581,10 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */, BCB613842C68703800485544 /* NodePositionIntent.swift */, BCB613862C69A0FB00485544 /* AppIntentErrors.swift */, + BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */, + BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */, + BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */, + BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, ); path = AppIntents; sourceTree = ""; @@ -1257,6 +1269,7 @@ 25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */, 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */, + BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */, 259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */, 259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */, DDDB444829F8A9C900EE2349 /* String.swift in Sources */, @@ -1361,6 +1374,7 @@ D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, + BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, @@ -1421,6 +1435,7 @@ BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */, + BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */, 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */, DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */, @@ -1436,6 +1451,7 @@ DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */, DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */, + BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, diff --git a/Meshtastic/AppIntents/AppIntentErrors.swift b/Meshtastic/AppIntents/AppIntentErrors.swift index c20ead7c..8e80b6b6 100644 --- a/Meshtastic/AppIntents/AppIntentErrors.swift +++ b/Meshtastic/AppIntents/AppIntentErrors.swift @@ -6,6 +6,7 @@ // import Foundation +import OSLog class AppIntentErrors { enum AppIntentError: Swift.Error, CustomLocalizedStringResourceConvertible { @@ -14,8 +15,12 @@ class AppIntentErrors { var localizedStringResource: LocalizedStringResource { switch self { - case let .message(message): return "Error: \(message)" - case .notConnected: return "No Connected Node" + case let .message(message): + Logger.services.error("App Intent: \(message)") + return "Error: \(message)" + case .notConnected: + Logger.services.error("App Intent: No Connected Node") + return "No Connected Node" } } } diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift new file mode 100644 index 00000000..f83a507b --- /dev/null +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -0,0 +1,41 @@ +// +// FactoryResetNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/25/24. +// + +import Foundation +import AppIntents + +struct FactoryResetNodeIntent: AppIntent { + static var title: LocalizedStringResource = "Factory Reset Node" + static var description: IntentDescription = "Perform a factory reset on the node you are connected to" + + func perform() async throws -> some IntentResult { + // Request user confirmation before performing the factory reset + try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"),confirmationActionName: ConfirmationActionName + .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + + // Ensure the node is connected + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + // Safely unwrap the connected node information + if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), + let fromUser = connectedNode.user, + let toUser = connectedNode.user { + + // Attempt to send a factory reset command, throw an error if it fails + if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) { + throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset") + } + } else { + throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") + } +// + return .result() + } +} diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index 53e202a6..4f104285 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -9,7 +9,7 @@ import Foundation import AppIntents struct MessageChannelIntent: AppIntent { - static var title: LocalizedStringResource = "Send a channel message" + static var title: LocalizedStringResource = "Send a Channel Message" static var description: IntentDescription = "Send a message to a certain meshtastic channel" diff --git a/Meshtastic/AppIntents/NodePositionIntent.swift b/Meshtastic/AppIntents/NodePositionIntent.swift index 496ca3e3..a173df0d 100644 --- a/Meshtastic/AppIntents/NodePositionIntent.swift +++ b/Meshtastic/AppIntents/NodePositionIntent.swift @@ -31,6 +31,7 @@ struct NodePositionIntent: AppIntent { } let nodeInfo = fetchedNode[0] + nodeInfo.latestEnvironmentMetrics?.batteryLevel if let latitude = nodeInfo.latestPosition?.coordinate.latitude, let longitude = nodeInfo.latestPosition?.coordinate.longitude { let nodeLocation = CLLocation(latitude: latitude, longitude: longitude) diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift new file mode 100644 index 00000000..9bf8fd87 --- /dev/null +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -0,0 +1,42 @@ +// +// RestartNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/24/24. +// + +import Foundation +import AppIntents + +struct RestartNodeIntent: AppIntent { + static var title: LocalizedStringResource = "Restart Node" + + static var description: IntentDescription = "Restart to the node you are connected to" + + + func perform() async throws -> some IntentResult { + + try await requestConfirmation(result: .result(dialog: "Reboot Node?")) + + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + // Safely unwrap the connectedNode using if let + if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), + let fromUser = connectedNode.user, + let toUser = connectedNode.user, + let adminIndex = connectedNode.myInfo?.adminIndex { + + // Attempt to send shutdown, throw an error if it fails + if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { + throw AppIntentErrors.AppIntentError.message("Failed to restart") + } + } else { + throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") + } + + return .result() + } +} + diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index a94e8b7d..392d232a 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -12,7 +12,7 @@ import MeshtasticProtobufs struct SendWaypointIntent: AppIntent { - static var title = LocalizedStringResource("Send a waypoint") + static var title = LocalizedStringResource("Send a Waypoint") @Parameter(title: "Name", default: "Dropped Pin") var nameParameter: String? diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift new file mode 100644 index 00000000..6a82f3f5 --- /dev/null +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -0,0 +1,37 @@ +// +// ShortcutsProvider.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/24/24. +// + +import Foundation +import AppIntents + +struct ShortcutsProvider: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut(intent: ShutDownNodeIntent(), + phrases: ["Shut down node in \(.applicationName)", + "Turn off node in \(.applicationName)", + "Power down node in \(.applicationName)", + "Deactivate node in \(.applicationName)"], + shortTitle: "Shut Down Node", + systemImageName: "power") + + AppShortcut(intent: RestartNodeIntent(), + phrases: ["Restart node in \(.applicationName)", + "Reboot node in \(.applicationName)", + "Reset node in \(.applicationName)", + "Start node again in \(.applicationName)"], + shortTitle: "Restart Node", + systemImageName: "arrow.circlepath") + + AppShortcut(intent: MessageChannelIntent(), + phrases: ["Message channel in \(.applicationName)",], + shortTitle: "Message Channel", + systemImageName: "message") + } +} + + + diff --git a/Meshtastic/AppIntents/ShutDownNodeIntent.swift b/Meshtastic/AppIntents/ShutDownNodeIntent.swift new file mode 100644 index 00000000..ea57548d --- /dev/null +++ b/Meshtastic/AppIntents/ShutDownNodeIntent.swift @@ -0,0 +1,41 @@ +// +// ShutDownNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/24/24. +// + +import Foundation +import AppIntents + +struct ShutDownNodeIntent: AppIntent { + static var title: LocalizedStringResource = "Shut Down Node" + + static var description: IntentDescription = "Send a shutdown to the node you are connected to" + + + func perform() async throws -> some IntentResult { + try await requestConfirmation(result: .result(dialog: "Shut Down Node?")) + + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + // Safely unwrap the connectedNode using if let + if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), + let fromUser = connectedNode.user, + let toUser = connectedNode.user, + let adminIndex = connectedNode.myInfo?.adminIndex { + + // Attempt to send shutdown, throw an error if it fails + if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { + throw AppIntentErrors.AppIntentError.message("Failed to shut down") + } + } else { + throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") + } + + return .result() + } +} From d3a7de2f764f90b3b3f4c0681ffdbee26b907fa4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 25 Aug 2024 23:02:43 -0700 Subject: [PATCH 164/333] Seperate apps settings and data clearing, update logic for online nodes text --- Localizable.xcstrings | 24 ++++- .../CoreData/NodeInfoEntityExtension.swift | 6 +- Meshtastic/Views/Bluetooth/Connect.swift | 92 ++++++++++++++----- .../Views/Messages/ChannelMessageList.swift | 3 - Meshtastic/Views/Settings/AppSettings.swift | 4 +- Meshtastic/Views/Settings/Channels.swift | 9 +- .../Views/Settings/Channels/ChannelForm.swift | 24 ++++- .../Settings/Config/Module/MQTTConfig.swift | 15 +-- Widgets/WidgetsLiveActivity.swift | 42 ++++++--- 9 files changed, 159 insertions(+), 60 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9989f2b7..e6c29271 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -140,6 +140,16 @@ }, "%@%%" : { + }, + "%@%% %@%%" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@%% %2$@%%" + } + } + } }, "%@°F" : { @@ -6712,6 +6722,9 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { + }, + "Dupe / Bad Packets: %d" : { + }, "echo" : { "localizations" : { @@ -16072,6 +16085,12 @@ }, "Override automatic OLED screen detection." : { + }, + "Packets Received: %d" : { + + }, + "Packets Sent: %d" : { + }, "password" : { "localizations" : { @@ -17387,6 +17406,9 @@ }, "Requires that there be an accelerometer on your device." : { + }, + "Reset App Settings" : { + }, "Reset NodeDB" : { @@ -22679,7 +22701,7 @@ "Your Firmware is up to date" : { }, - "Your MQTT Server must support TLS." : { + "Your MQTT Server must support TLS. Not available via the public mqtt server." : { }, "Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : { diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index ba8e38fa..2effa9ae 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -22,9 +22,9 @@ extension NodeInfoEntity { return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity } - var latestLocalStats: TelemetryEntity? { - return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity - } +// var latestLocalStats: TelemetryEntity? { +// return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity +// } var hasPositions: Bool { return positions?.count ?? 0 > 0 diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 58d8757b..a7c7d5b8 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -52,38 +52,80 @@ struct Connect: View { if #available(iOS 17.0, macOS 14.0, *) { TipView(BluetoothConnectionTip(), arrowEdge: .bottom) } - HStack { - VStack(alignment: .center) { - CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90) - } - .padding(.trailing) - VStack(alignment: .leading) { - if node != nil { - Text(connectedPeripheral.longName).font(.title2) + VStack(alignment: .leading) { + HStack { + VStack(alignment: .center) { + CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90) + .padding(.trailing, 5) + if node?.latestDeviceMetrics != nil { + BatteryCompact(batteryLevel: node?.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor) + .padding(.trailing, 5) + } } - Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)") - .font(.callout).foregroundColor(Color.gray) - if node != nil { - Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)") + .padding(.trailing) + VStack(alignment: .leading) { + if node != nil { + Text(connectedPeripheral.longName).font(.title2) + } + Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)") .font(.callout).foregroundColor(Color.gray) - } - if bleManager.isSubscribed { - Text("subscribed").font(.callout) - .foregroundColor(.green) - } else { - - HStack { - if #available(iOS 17.0, macOS 14.0, *) { - Image(systemName: "square.stack.3d.down.forward") - .symbolRenderingMode(.multicolor) - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + if node != nil { + Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)") + .font(.callout).foregroundColor(Color.gray) + } + if bleManager.isSubscribed { + Text("subscribed").font(.callout) + .foregroundColor(.green) + } else { + HStack { + if #available(iOS 17.0, macOS 14.0, *) { + Image(systemName: "square.stack.3d.down.forward") + .symbolRenderingMode(.multicolor) + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .foregroundColor(.orange) + } + Text("communicating").font(.callout) .foregroundColor(.orange) } - Text("communicating").font(.callout) - .foregroundColor(.orange) } } } + VStack { + let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity + if localStats != nil { + Divider() + if localStats?.numTotalNodes ?? 0 >= 100 { + Text("\(String(format: "Connected: %d nodes online", localStats?.numOnlineNodes ?? 0))") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } else { + Text("\(String(format: "Connected: %d of %d nodes online", localStats?.numOnlineNodes ?? 0, localStats?.numTotalNodes ?? 0))") + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } + Text("\(String(format: "Ch. Util: %.2f", localStats?.channelUtilization ?? 0))% \(String(format: "Airtime: %.2f", localStats?.airUtilTx ?? 0))%") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + Text("Packets Sent: \(localStats?.numPacketsTx ?? 0)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + Text("Packets Received: \(localStats?.numPacketsRx ?? 0)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + Text("Dupe / Bad Packets: \(localStats?.numPacketsRxBad ?? 0)") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } + } } .font(.caption) .foregroundColor(Color.gray) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 91a922c9..36cc5592 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -98,9 +98,6 @@ struct ChannelMessageList: View { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) .foregroundStyle(ackErrorVal?.color ?? Color.red) .font(.caption2) - } else { - let messageDate = message.timestamp - Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) } } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 02286655..0bb86bc1 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -76,9 +76,11 @@ struct AppSettings: View { } clearCoreDataDatabase(context: context, includeRoutes: true) context.refreshAllObjects() - UserDefaults.standard.reset() } } + Button("Reset App Settings", systemImage: "clipboard") { + UserDefaults.standard.reset() + } } if totalDownloadedTileSize != "0MB" { Section(header: Text("Map Tile Data")) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index ce48b658..3bb6ab38 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -98,7 +98,12 @@ struct Channels: View { preciseLocation = false positionsEnabled = false } else { - if positionPrecision == 32 { + if channelKey == "AQ==" { + preciseLocation = false + if positionPrecision < 10 || positionPrecision > 16 { + positionPrecision = 13 + } + } else if positionPrecision == 32 { preciseLocation = true positionsEnabled = true } else { @@ -250,7 +255,7 @@ struct Channels: View { channelKey = key positionsEnabled = false preciseLocation = false - positionPrecision = 0 + positionPrecision = 13 uplink = false downlink = false hasChanges = true diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 780a19b4..28fe1116 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -157,11 +157,20 @@ struct ChannelForm: View { if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 10...19, step: 1) { - } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { - Image(systemName: "plus") + if channelKeySize == -1 { + Slider(value: $positionPrecision, in: 10...16, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") + } + } else { + Slider(value: $positionPrecision, in: 10...19, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") + } } Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "") .foregroundColor(.gray) @@ -199,6 +208,11 @@ struct ChannelForm: View { .onChange(of: channelKey) { _ in hasChanges = true } + .onChange(of: channelKeySize) { _ in + if channelKeySize == -1 { + preciseLocation = false + } + } .onChange(of: channelRole) { _ in hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index d6176526..8ef411ea 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -24,7 +24,7 @@ struct MQTTConfig: View { @State var password = "" @State var encryptionEnabled = true @State var jsonEnabled = false - @State var tlsEnabled = true + @State var tlsEnabled = false @State var root = "msh" @State var selectedTopic = "" @State var mqttConnected: Bool = false @@ -234,7 +234,7 @@ struct MQTTConfig: View { .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) Toggle(isOn: $tlsEnabled) { Label("TLS Enabled", systemImage: "checkmark.shield.fill") - Text("Your MQTT Server must support TLS.") + Text("Your MQTT Server must support TLS. Not available via the public mqtt server.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } @@ -288,9 +288,6 @@ struct MQTTConfig: View { jsonEnabled = false } if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true } - if newProxyToClientEnabled { - jsonEnabled = false - } } .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { @@ -324,8 +321,12 @@ struct MQTTConfig: View { } if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true } } - .onChange(of: tlsEnabled) { - if $0 != node?.mqttConfig?.tlsEnabled { hasChanges = true } + .onChange(of: tlsEnabled) { newTlsEnabled in + if address.lowercased() == "mqtt.meshtastic.org" { + tlsEnabled = false + } else { + if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true } + } } .onChange(of: mqttConnected) { newMqttConnected in if newMqttConnected == false { diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 0235f566..14d9762f 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -23,7 +23,7 @@ struct WidgetsLiveActivity: Widget { nodesOnline: context.state.nodesOnline, totalNodes: context.state.totalNodes, timerRange: context.state.timerRange) - .widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)")) + .widgetURL(URL(string: "meshtastic:///bluetooth")) } dynamicIsland: { context in DynamicIsland { @@ -38,10 +38,17 @@ struct WidgetsLiveActivity: Widget { .fixedSize() Spacer() } - Text("\(context.state.nodesOnline) nodes online") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize() + if context.state.nodesOnline >= 100 { + Text("100+ online") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + } else { + Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + } Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") .font(.caption) .foregroundStyle(.secondary) @@ -74,7 +81,7 @@ struct WidgetsLiveActivity: Widget { .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Text("Dupe / Bad: \(context.state.badReceivedPackets)") + Text("Dupe / Bad \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() @@ -108,7 +115,7 @@ struct WidgetsLiveActivity: Widget { .contentMargins(.trailing, 32, for: .expanded) .contentMargins([.leading, .top, .bottom], 6, for: .compactLeading) .contentMargins(.all, 6, for: .minimal) - .widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)")) + .widgetURL(URL(string: "meshtastic:///bluetooth")) } } } @@ -214,12 +221,21 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("\(String(format: "Connected: %d nodes online", nodesOnline, totalNodes))") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() + if totalNodes >= 100 { + Text("\(String(format: "Connected: %d nodes online", nodesOnline))") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } else { + Text("\(String(format: "Connected: %d of %d nodes online", nodesOnline, totalNodes))") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } let now = Date() Text("Last Heard: \(now.formatted())") .font(.caption) From 698eb7b5d08519ba11236739dc9daf96b2b83ad9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 26 Aug 2024 22:31:03 -0700 Subject: [PATCH 165/333] Update app settings clear button --- Meshtastic/Views/Settings/AppSettings.swift | 5 +++- Meshtastic/Views/Settings/Channels.swift | 6 ++++- .../Views/Settings/Channels/ChannelForm.swift | 27 +++++++------------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 0bb86bc1..f641b77b 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -78,8 +78,11 @@ struct AppSettings: View { context.refreshAllObjects() } } - Button("Reset App Settings", systemImage: "clipboard") { + Button { UserDefaults.standard.reset() + } label: { + Label("Reset App Settings", systemImage: "arrow.counterclockwise.circle") + .foregroundColor(.red) } } if totalDownloadedTileSize != "0MB" { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 3bb6ab38..61cddcea 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -92,7 +92,11 @@ struct Channels: View { positionPrecision = 32 preciseLocation = true positionsEnabled = true - + if channelKey == "AQ==" { + positionPrecision = 13 + preciseLocation = false + positionsEnabled = true + } } else if !supportedVersion && channelRole == 2 { positionPrecision = 0 preciseLocation = false diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 28fe1116..52e1f59e 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -157,20 +157,12 @@ struct ChannelForm: View { if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - if channelKeySize == -1 { - Slider(value: $positionPrecision, in: 10...16, step: 1) { - } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { - Image(systemName: "plus") - } - } else { - Slider(value: $positionPrecision, in: 10...19, step: 1) { - } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { - Image(systemName: "plus") - } + + Slider(value: $positionPrecision, in: 10...16, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") } Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "") .foregroundColor(.gray) @@ -211,6 +203,7 @@ struct ChannelForm: View { .onChange(of: channelKeySize) { _ in if channelKeySize == -1 { preciseLocation = false + channelKey = "AQ==" } } .onChange(of: channelRole) { _ in @@ -220,7 +213,7 @@ struct ChannelForm: View { if loc == true { positionPrecision = 32 } else { - positionPrecision = 14 + positionPrecision = 13 } hasChanges = true } @@ -230,7 +223,7 @@ struct ChannelForm: View { .onChange(of: positionsEnabled) { pe in if pe { if positionPrecision == 0 { - positionPrecision = 32 + positionPrecision = 13 } } else { positionPrecision = 0 @@ -243,7 +236,7 @@ struct ChannelForm: View { .onChange(of: downlink) { _ in hasChanges = true } - .onAppear { + .onFirstAppear { let tempKey = Data(base64Encoded: channelKey) ?? Data() if tempKey.count == channelKeySize || channelKeySize == -1 { hasValidKey = true From f757909aa44dac83c741d1714836abf0d8316a22 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 26 Aug 2024 22:53:12 -0700 Subject: [PATCH 166/333] remove local stats call --- Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 2effa9ae..c1bd5bec 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -22,10 +22,6 @@ extension NodeInfoEntity { return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity } -// var latestLocalStats: TelemetryEntity? { -// return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity -// } - var hasPositions: Bool { return positions?.count ?? 0 > 0 } From 3df98d4345991b9e9b29ac9ebe079cc1813f268a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 26 Aug 2024 23:46:06 -0700 Subject: [PATCH 167/333] Linting errors --- Meshtastic/AppIntents/ShortcutsProvider.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift index 6a82f3f5..e0f96ec3 100644 --- a/Meshtastic/AppIntents/ShortcutsProvider.swift +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -25,13 +25,10 @@ struct ShortcutsProvider: AppShortcutsProvider { "Start node again in \(.applicationName)"], shortTitle: "Restart Node", systemImageName: "arrow.circlepath") - + AppShortcut(intent: MessageChannelIntent(), - phrases: ["Message channel in \(.applicationName)",], - shortTitle: "Message Channel", - systemImageName: "message") + phrases: ["Message channel in \(.applicationName)"], + shortTitle: "Message Channel", + systemImageName: "message") } } - - - From b188e3c18f91b8ca69deccdce8dee16707f2bfa2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 27 Aug 2024 20:44:17 -0700 Subject: [PATCH 168/333] update channel form --- Meshtastic/Views/Settings/Channels/ChannelForm.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 52e1f59e..d44d959e 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -211,6 +211,11 @@ struct ChannelForm: View { } .onChange(of: preciseLocation) { loc in if loc == true { + if channelKey == "AQ==" { + preciseLocation = false + } else { + positionPrecision = 32 + } positionPrecision = 32 } else { positionPrecision = 13 From eec7f593c502a18a3d9db9aeb5ce471f6edeba78 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 27 Aug 2024 21:17:35 -0700 Subject: [PATCH 169/333] Clean up labels and siri phrases --- Localizable.xcstrings | 15 +++++------ .../AppIntents/FactoryResetNodeIntent.swift | 2 +- .../AppIntents/MessageChannelIntent.swift | 2 +- Meshtastic/AppIntents/RestartNodeIntent.swift | 9 +++---- Meshtastic/AppIntents/ShortcutsProvider.swift | 26 ++++++++++--------- .../AppIntents/ShutDownNodeIntent.swift | 6 ++--- 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3ae57f93..cf5fc247 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7225,9 +7225,6 @@ }, "Factory Reset" : { - }, - "Factory Reset Node" : { - }, "Factory reset your device and app? " : { @@ -8167,6 +8164,9 @@ } } } + }, + "Group Message" : { + }, "Gusts %@" : { @@ -14582,9 +14582,6 @@ }, "Message" : { - }, - "Message Channel" : { - }, "Message content exceeds 228 bytes." : { @@ -17428,7 +17425,7 @@ "Reset NodeDB" : { }, - "Restart Node" : { + "Restart" : { }, "Restart to the node you are connected to" : { @@ -19175,7 +19172,7 @@ "Send" : { }, - "Send a Channel Message" : { + "Send a Group Message" : { }, "Send a message to a certain meshtastic channel" : { @@ -19924,7 +19921,7 @@ "Show Weather" : { }, - "Shut Down Node" : { + "Shut Down" : { }, "Shut Down Node?" : { diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift index f83a507b..7ff2bd92 100644 --- a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -9,7 +9,7 @@ import Foundation import AppIntents struct FactoryResetNodeIntent: AppIntent { - static var title: LocalizedStringResource = "Factory Reset Node" + static var title: LocalizedStringResource = "Factory Reset" static var description: IntentDescription = "Perform a factory reset on the node you are connected to" func perform() async throws -> some IntentResult { diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index 4f104285..29d0a6c2 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -9,7 +9,7 @@ import Foundation import AppIntents struct MessageChannelIntent: AppIntent { - static var title: LocalizedStringResource = "Send a Channel Message" + static var title: LocalizedStringResource = "Send a Group Message" static var description: IntentDescription = "Send a message to a certain meshtastic channel" diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift index 9bf8fd87..7ae8095a 100644 --- a/Meshtastic/AppIntents/RestartNodeIntent.swift +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -9,13 +9,12 @@ import Foundation import AppIntents struct RestartNodeIntent: AppIntent { - static var title: LocalizedStringResource = "Restart Node" + static var title: LocalizedStringResource = "Restart" static var description: IntentDescription = "Restart to the node you are connected to" - func perform() async throws -> some IntentResult { - + try await requestConfirmation(result: .result(dialog: "Reboot Node?")) if !BLEManager.shared.isConnected { @@ -27,7 +26,7 @@ struct RestartNodeIntent: AppIntent { let fromUser = connectedNode.user, let toUser = connectedNode.user, let adminIndex = connectedNode.myInfo?.adminIndex { - + // Attempt to send shutdown, throw an error if it fails if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { throw AppIntentErrors.AppIntentError.message("Failed to restart") @@ -35,8 +34,6 @@ struct RestartNodeIntent: AppIntent { } else { throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") } - return .result() } } - diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift index e0f96ec3..b21c7e7d 100644 --- a/Meshtastic/AppIntents/ShortcutsProvider.swift +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -11,24 +11,26 @@ import AppIntents struct ShortcutsProvider: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut(intent: ShutDownNodeIntent(), - phrases: ["Shut down node in \(.applicationName)", - "Turn off node in \(.applicationName)", - "Power down node in \(.applicationName)", - "Deactivate node in \(.applicationName)"], - shortTitle: "Shut Down Node", + phrases: ["Shut down \(.applicationName) node", + "Shut down my \(.applicationName) node", + "Turn off \(.applicationName) node", + "Power down \(.applicationName) node", + "Deactivate \(.applicationName) node"], + shortTitle: "Shut Down", systemImageName: "power") AppShortcut(intent: RestartNodeIntent(), - phrases: ["Restart node in \(.applicationName)", - "Reboot node in \(.applicationName)", - "Reset node in \(.applicationName)", - "Start node again in \(.applicationName)"], - shortTitle: "Restart Node", + phrases: ["Restart \(.applicationName) node", + "Restart my \(.applicationName) node", + "Reboot \(.applicationName) node", + "Reboot my \(.applicationName) node"], + shortTitle: "Restart", systemImageName: "arrow.circlepath") AppShortcut(intent: MessageChannelIntent(), - phrases: ["Message channel in \(.applicationName)"], - shortTitle: "Message Channel", + phrases: ["Message a \(.applicationName) channel", + "Send a \(.applicationName) group message"], + shortTitle: "Group Message", systemImageName: "message") } } diff --git a/Meshtastic/AppIntents/ShutDownNodeIntent.swift b/Meshtastic/AppIntents/ShutDownNodeIntent.swift index ea57548d..dcb43f3c 100644 --- a/Meshtastic/AppIntents/ShutDownNodeIntent.swift +++ b/Meshtastic/AppIntents/ShutDownNodeIntent.swift @@ -9,10 +9,9 @@ import Foundation import AppIntents struct ShutDownNodeIntent: AppIntent { - static var title: LocalizedStringResource = "Shut Down Node" + static var title: LocalizedStringResource = "Shut Down" static var description: IntentDescription = "Send a shutdown to the node you are connected to" - func perform() async throws -> some IntentResult { try await requestConfirmation(result: .result(dialog: "Shut Down Node?")) @@ -27,7 +26,7 @@ struct ShutDownNodeIntent: AppIntent { let fromUser = connectedNode.user, let toUser = connectedNode.user, let adminIndex = connectedNode.myInfo?.adminIndex { - + // Attempt to send shutdown, throw an error if it fails if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { throw AppIntentErrors.AppIntentError.message("Failed to shut down") @@ -35,7 +34,6 @@ struct ShutDownNodeIntent: AppIntent { } else { throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data") } - return .result() } } From 620e329521a6b9059a8b0f603cc7d4a5c835f26a Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Wed, 28 Aug 2024 07:02:55 -0500 Subject: [PATCH 170/333] Refactor router state to more closely match tab bar behavior --- Meshtastic/Router/NavigationState.swift | 47 +------ Meshtastic/Router/Router.swift | 30 +++-- Meshtastic/Views/ContentView.swift | 9 +- Meshtastic/Views/Messages/Messages.swift | 34 ++--- Meshtastic/Views/Nodes/MeshMap.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 7 +- Meshtastic/Views/Settings/Settings.swift | 7 +- MeshtasticTests/RouterTests.swift | 151 ++++++++++++++++++----- 8 files changed, 158 insertions(+), 129 deletions(-) diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index 0ff61108..0a85f4ab 100644 --- a/Meshtastic/Router/NavigationState.swift +++ b/Meshtastic/Router/NavigationState.swift @@ -55,17 +55,7 @@ enum SettingsNavigationState: String { case firmwareUpdates } -enum NavigationState: Hashable { - case messages(MessagesNavigationState? = nil) - case bluetooth - case nodes(selectedNodeNum: Int64? = nil) - case map(MapNavigationState? = nil) - case settings(SettingsNavigationState? = nil) -} - -// MARK: Tab Bar - -extension NavigationState { +struct NavigationState: Hashable { enum Tab: String, Hashable { case messages case bluetooth @@ -74,34 +64,9 @@ extension NavigationState { case settings } - var tab: Tab { - get { - switch self { - case .messages: - .messages - case .bluetooth: - .bluetooth - case .nodes: - .nodes - case .map: - .map - case .settings: - .settings - } - } - set { - self = switch newValue { - case .messages: - .messages() - case .bluetooth: - .bluetooth - case .nodes: - .nodes() - case .map: - .map() - case .settings: - .settings() - } - } - } + var selectedTab: Tab = .bluetooth + var messages: MessagesNavigationState? + var nodeListSelectedNodeNum: Int64? + var map: MapNavigationState? + var settings: SettingsNavigationState? } diff --git a/Meshtastic/Router/Router.swift b/Meshtastic/Router/Router.swift index 51803ed4..718c71b1 100644 --- a/Meshtastic/Router/Router.swift +++ b/Meshtastic/Router/Router.swift @@ -12,7 +12,9 @@ class Router: ObservableObject { private var cancellables: Set = [] init( - navigationState: NavigationState = .bluetooth + navigationState: NavigationState = NavigationState( + selectedTab: .bluetooth + ) ) { self.navigationState = navigationState @@ -21,10 +23,6 @@ class Router: ObservableObject { }.store(in: &cancellables) } - func route(to destination: NavigationState) { - navigationState = destination - } - func route(url: URL) { guard url.scheme == "meshtastic" else { Logger.services.error("🛣 Received routing URL \(url, privacy: .public) with invalid scheme. Ignoring route.") @@ -38,7 +36,7 @@ class Router: ObservableObject { if components.path == "/messages" { routeMessages(components) } else if components.path == "/bluetooth" { - route(to: .bluetooth) + navigationState.selectedTab = .bluetooth } else if components.path == "/nodes" { routeNodes(components) } else if components.path == "/map" { @@ -75,7 +73,8 @@ class Router: ObservableObject { } else { nil } - route(to: .messages(state)) + navigationState.selectedTab = .messages + navigationState.messages = state } private func routeNodes(_ components: URLComponents) { @@ -83,7 +82,9 @@ class Router: ObservableObject { .first(where: { $0.name == "nodenum" })? .value .flatMap(Int64.init) - route(to: .nodes(selectedNodeNum: nodeId)) + + navigationState.selectedTab = .nodes + navigationState.nodeListSelectedNodeNum = nodeId } private func routeMap(_ components: URLComponents) { @@ -95,12 +96,14 @@ class Router: ObservableObject { .first(where: { $0.name == "waypointId" })? .value .flatMap(Int64.init) - if let nodeId { - route(to: .map(.selectedNode(nodeId))) + + navigationState.selectedTab = .map + navigationState.map = if let nodeId { + .selectedNode(nodeId) } else if let waypointId { - route(to: .map(.waypoint(waypointId))) + .waypoint(waypointId) } else { - route(to: .map()) + nil } } @@ -112,6 +115,7 @@ class Router: ObservableObject { .flatMap(String.init) .flatMap(SettingsNavigationState.init(rawValue:)) - route(to: .settings(settingFromPath)) + navigationState.selectedTab = .settings + navigationState.settings = settingFromPath } } diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index e8384c35..23b25f0c 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -13,14 +13,7 @@ struct ContentView: View { var router: Router var body: some View { - TabView(selection: Binding( - get: { - appState.router.navigationState.tab - }, - set: { newValue in - appState.router.navigationState.tab = newValue - } - )) { + TabView(selection: $appState.router.navigationState.selectedTab) { Messages( router: appState.router, unreadChannelMessages: $appState.unreadChannelMessages, diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 88a6f2a2..09b8d1af 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -26,21 +26,6 @@ struct Messages: View { @Binding var unreadDirectMessages: Int - // Aliases the navigation state for the NavigationSplitView sidebar selection - private var messagesSelection: Binding { - Binding( - get: { - guard case .messages(let state) = router.navigationState else { - return nil - } - return state - }, - set: { newValue in - router.navigationState = .messages(newValue) - } - ) - } - @State var node: NodeInfoEntity? @State private var userSelection: UserEntity? // Nothing selected by default. @State private var channelSelection: ChannelEntity? // Nothing selected by default. @@ -49,7 +34,7 @@ struct Messages: View { var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { - List(selection: messagesSelection) { + List(selection: $router.navigationState.messages) { NavigationLink(value: MessagesNavigationState.channels()) { Label { Text("channels") @@ -88,11 +73,12 @@ struct Messages: View { .navigationBarTitleDisplayMode(.large) .navigationBarItems(leading: MeshtasticLogo()) } content: { - if case .messages(.channels) = router.navigationState { + switch router.navigationState.messages { + case .channels(let channelId, let messageId): ChannelList(node: $node, channelSelection: $channelSelection) - } else if case .messages(.directMessages) = router.navigationState { + case .directMessages(let userNum, let messageId): UserList(node: $node, userSelection: $userSelection) - } else if case .messages(nil) = router.navigationState { + case nil: Text("Select a conversation type") } } detail: { @@ -100,9 +86,9 @@ struct Messages: View { ChannelMessageList(myInfo: myInfo, channel: channelSelection) } else if let userSelection { UserMessageList(user: userSelection) - } else if case .messages(.channels) = router.navigationState { + } else if case .channels = router.navigationState.messages { Text("Select a channel") - } else if case .messages(.directMessages) = router.navigationState { + } else if case .directMessages = router.navigationState.messages { Text("Select a conversation") } }.onChange(of: router.navigationState) { _ in @@ -116,11 +102,7 @@ struct Messages: View { node = getNodeInfo(id: nodeId, context: context) } - guard case .messages(let state) = router.navigationState else { - return - } - - guard let state else { + guard let state = router.navigationState.messages else { channelSelection = nil userSelection = nil return diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index f471caaf..595abb15 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -123,7 +123,7 @@ struct MeshMap: View { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) } .onChange(of: router.navigationState) { - guard case .map(let selectedNodeNum) = router.navigationState else { return } + guard case .map = router.navigationState.selectedTab else { return } // TODO: handle deep link for waypoints } .onChange(of: selectedMapLayer) { newMapLayer in diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c0f16dd8..7df0283c 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -335,11 +335,8 @@ struct NodeList: View { } } .onChange(of: router.navigationState) { _ in - // Handle deep link routing - if case .nodes(let selected) = router.navigationState { - self.selectedNode = selected.flatMap { - getNodeInfo(id: $0, context: context) - } + if let selected = router.navigationState.nodeListSelectedNodeNum { + self.selectedNode = getNodeInfo(id: selected, context: context) } else { self.selectedNode = nil } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 814f3ab9..e88816ab 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -299,13 +299,10 @@ struct Settings: View { NavigationStack( path: Binding<[SettingsNavigationState]>( get: { - guard case .settings(let route) = router.navigationState, let setting = route else { - return [] - } - return [setting] + [router.navigationState.settings].compactMap { $0 } }, set: { newPath in - router.navigationState = .settings(newPath.first) + router.navigationState.settings = newPath.first } ) ) { diff --git a/MeshtasticTests/RouterTests.swift b/MeshtasticTests/RouterTests.swift index 81b07276..a3ecee6d 100644 --- a/MeshtasticTests/RouterTests.swift +++ b/MeshtasticTests/RouterTests.swift @@ -5,53 +5,144 @@ import XCTest final class RouterTests: XCTestCase { - func testInitialState() throws { - XCTAssertEqual(Router().navigationState, .bluetooth) + func testInitialState() async throws { + let router = await Router() + let tab = await router.navigationState.selectedTab + XCTAssertEqual(tab, .bluetooth) } - func testRouteTo() throws { - let router = Router(navigationState: .bluetooth) - router.route(to: .settings(.about)) - XCTAssertEqual(router.navigationState, .settings(.about)) + func testRouteMessages() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///messages", + NavigationState(selectedTab: .messages) + ) } - func testRouteURL() throws { - // Messages - try assertRoute("meshtastic:///messages", .messages()) - try assertRoute( + func testRouteMessagesWithChannelIdAndMessageId() async throws { + try await assertRoute( + router: Router(), "meshtastic:///messages?channelId=0&messageId=1122334455", - .messages(.channels(channelId: 0, messageId: 1122334455)) + NavigationState( + selectedTab: .messages, + messages: .channels( + channelId: 0, + messageId: 1122334455 + ) + ) ) - try assertRoute( + } + + func testRouteMessagesWithUserNumAndMessageId() async throws { + try await assertRoute( + router: Router(), "meshtastic:///messages?userNum=123456789&messageId=9876543210", - .messages(.directMessages(userNum: 123456789, messageId: 9876543210)) + NavigationState( + selectedTab: .messages, + messages: .directMessages( + userNum: 123456789, + messageId: 9876543210 + ) + ) ) + } - // Bluetooth - try assertRoute("meshtastic:///bluetooth", .bluetooth) + func testRouteBluetooth() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///bluetooth", + NavigationState(selectedTab: .bluetooth) + ) + } - // Nodes - try assertRoute("meshtastic:///nodes", .nodes()) - try assertRoute("meshtastic:///nodes?nodenum=1234567890", .nodes(selectedNodeNum: 1234567890)) + func testRouteNodes() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///nodes", + NavigationState(selectedTab: .nodes) + ) + } - // Map - try assertRoute("meshtastic:///map", .map()) - try assertRoute("meshtastic:///map?waypointId=123456", .map(.waypoint(123456))) - try assertRoute("meshtastic:///map?nodenum=1234567890", .map(.selectedNode(1234567890))) + func testRouteNodesWithNodeNum() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///nodes?nodenum=1234567890", + NavigationState( + selectedTab: .nodes, + nodeListSelectedNodeNum: 1234567890 + ) + ) + } - // Settings - try assertRoute("meshtastic:///settings", .settings()) - try assertRoute("meshtastic:///settings/about", .settings(.about)) - try assertRoute("meshtastic:///settings/invalidSetting", .settings()) + func testRouteMap() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///map", + NavigationState(selectedTab: .map) + ) + } + + func testRouteMapWithWaypointId() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///map?waypointId=123456", + NavigationState( + selectedTab: .map, + map: .waypoint(123456) + ) + ) + } + + func testRouteMapWithNodeNum() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///map?nodenum=1234567890", + NavigationState( + selectedTab: .map, + map: .selectedNode(1234567890) + ) + ) + } + + func testRouteSettings() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///settings", + NavigationState( + selectedTab: .settings + ) + ) + } + + func testRouteSettingsAbout() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///settings/about", + NavigationState( + selectedTab: .settings, + settings: .about + ) + ) + } + + func testRouteSettingsInvalidSetting() async throws { + try await assertRoute( + router: Router(), + "meshtastic:///settings/invalidSetting", + NavigationState( + selectedTab: .settings + ) + ) } private func assertRoute( - router: Router = Router(), + router: Router, _ urlString: String, _ destination: NavigationState - ) throws { + ) async throws { let url = try XCTUnwrap(URL(string: urlString)) - router.route(url: url) - XCTAssertEqual(router.navigationState, destination) + await router.route(url: url) + let state = await router.navigationState + XCTAssertEqual(state, destination) } } From a8149e0c660428e227351ead416722b4ada6dc2d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 Aug 2024 08:36:45 -0700 Subject: [PATCH 171/333] Only send battery nofifications from the local node --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 12ed1f1b..f46600aa 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -757,7 +757,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage // Connected Device Metrics // ------------------------ // Low Battery notification - if connectedNode != Int64(packet.from) { + if connectedNode == Int64(packet.from) { if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { let manager = LocalNotificationManager() manager.notifications = [ From 583c992cb8b2f3376213cb28fec7ff305577787a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 Aug 2024 11:32:22 -0700 Subject: [PATCH 172/333] Remove publisher change event that runs way too often --- Meshtastic/Views/Messages/UserList.swift | 3 --- Meshtastic/Views/Nodes/NodeList.swift | 5 ----- 2 files changed, 8 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 7ba2205c..3a2c4a47 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -227,9 +227,6 @@ struct UserList: View { .onChange(of: maxDistance) { _ in searchUserList() } - .onReceive(users.publisher) { _ in - searchUserList() - } .onAppear { searchUserList() } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c0f16dd8..c5c92418 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -344,11 +344,6 @@ struct NodeList: View { self.selectedNode = nil } } - .onReceive(nodes.publisher) { _ in - Task { - await searchNodeList() - } - } .onAppear { Task { await searchNodeList() From 01b354e4a461f7ede2fc30e59716dcca1052b5d6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 29 Aug 2024 08:50:31 -0700 Subject: [PATCH 173/333] Ack error font --- Meshtastic/Views/Messages/UserMessageList.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index a53f788d..cb483745 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -84,6 +84,7 @@ struct UserMessageList: View { } else if currentUser && message.ackError > 0 { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) } } } From bda868bd1a9feec3e509e143b90f5c03dcfc1b4b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 30 Aug 2024 08:57:36 -0700 Subject: [PATCH 174/333] set position source --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Helpers/BLEManager.swift | 23 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 550bbc78..83fac71b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1686,7 +1686,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.2; + MARKETING_VERSION = 2.5.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1721,7 +1721,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.2; + MARKETING_VERSION = 2.5.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1753,7 +1753,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.2; + MARKETING_VERSION = 2.5.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1786,7 +1786,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.2; + MARKETING_VERSION = 2.5.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 2d8726ed..045a3694 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1153,7 +1153,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } @MainActor - public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { + public func getPositionFromPhoneGPS(destNum: Int64, fixedPosition: Bool) -> Position? { var positionPacket = Position() if #available(iOS 17.0, macOS 14.0, *) { @@ -1172,7 +1172,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) positionPacket.altitude = Int32(lastLocation.altitude) positionPacket.satsInView = UInt32(LocationsHandler.satsInView) - let currentSpeed = lastLocation.speed if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { positionPacket.groundSpeed = UInt32(currentSpeed) @@ -1181,6 +1180,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { positionPacket.groundTrack = UInt32(currentHeading) } + /// Set location source for time + if !fixedPosition { + /// From GPS treat time as good + positionPacket.locationSource = Position.LocSource.locExternal + } else { + /// From GPS, but time can be old and have drifted + positionPacket.locationSource = Position.LocSource.locManual + } } else { @@ -1199,6 +1206,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { positionPacket.groundTrack = UInt32(currentHeading) } + /// Set location source for time + if !fixedPosition { + /// From GPS treat time as good + positionPacket.locationSource = Position.LocSource.locExternal + } else { + /// From GPS, but time can be old and have drifted + positionPacket.locationSource = Position.LocSource.locManual + } } return positionPacket } @@ -1206,7 +1221,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @MainActor public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool { var adminPacket = AdminMessage() - guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else { + guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num, fixedPosition: true) else { return false } adminPacket.setFixedPosition = positionPacket @@ -1261,7 +1276,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @MainActor public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool { let fromNodeNum = connectedPeripheral.num - guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else { + guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum, fixedPosition: false) else { Logger.services.error("Unable to get position data from device GPS to send to node") return false } From ec2df295d17f89ace9a4e34520c9a8394d054267 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 31 Aug 2024 13:54:40 -0700 Subject: [PATCH 175/333] set time source properly --- Meshtastic/Helpers/BLEManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 045a3694..08b53801 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1329,7 +1329,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) var meshPacket: MeshPacket = MeshPacket() - meshPacket.to = 0 + meshPacket.to = UInt32(self.connectedPeripheral.num) + meshPacket.from = UInt32(self.connectedPeripheral.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Sat, 31 Aug 2024 15:04:25 -0700 Subject: [PATCH 176/333] Adjust precision on channel form --- Meshtastic/Views/Settings/Channels/ChannelForm.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index d44d959e..d0a111e5 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -202,7 +202,9 @@ struct ChannelForm: View { } .onChange(of: channelKeySize) { _ in if channelKeySize == -1 { - preciseLocation = false + if channelRole == 0 { + preciseLocation = false + } channelKey = "AQ==" } } @@ -211,7 +213,7 @@ struct ChannelForm: View { } .onChange(of: preciseLocation) { loc in if loc == true { - if channelKey == "AQ==" { + if channelKey == "AQ==" && channelRole == 0 { preciseLocation = false } else { positionPrecision = 32 From bff990357390cbedf03284524e501e7aeed96124 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 31 Aug 2024 15:35:26 -0700 Subject: [PATCH 177/333] Packets bad. GUVWAF has verified that these are bad packets, may have fixed a bug up stream in the process that was exposed by having the data available. --- Localizable.xcstrings | 6 +++--- Meshtastic/Views/Bluetooth/Connect.swift | 2 +- Widgets/WidgetsLiveActivity.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index cf5fc247..411af16d 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1428,6 +1428,9 @@ }, "Bad" : { + }, + "Bad Packets: %d" : { + }, "Bandwidth" : { @@ -6725,9 +6728,6 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { - }, - "Dupe / Bad Packets: %d" : { - }, "echo" : { "localizations" : { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index a7c7d5b8..2a1eea84 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -119,7 +119,7 @@ struct Connect: View { .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) - Text("Dupe / Bad Packets: \(localStats?.numPacketsRxBad ?? 0)") + Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 14d9762f..e6177129 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -81,7 +81,7 @@ struct WidgetsLiveActivity: Widget { .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Text("Dupe / Bad \(context.state.badReceivedPackets)") + Text("Bad \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() @@ -215,7 +215,7 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Dupe / Bad Packets: \(badReceivedPackets)") + Text("Bad Packets: \(badReceivedPackets)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) From 10293c617ac6c173951221d9a4fb8bba06485525 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 1 Sep 2024 00:56:24 -0700 Subject: [PATCH 178/333] Display both keys in the case of a key mismatch --- Localizable.xcstrings | 6 ++++++ Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 411af16d..7298c16e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15389,6 +15389,9 @@ }, "Never" : { + }, + "New Key%@" : { + }, "Newer firmware is available" : { @@ -16756,6 +16759,9 @@ }, "Public Key Mismatch" : { + }, + "Public Key%@" : { + }, "PWD" : { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 6724e805..f6e701a6 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -54,6 +54,12 @@ struct NodeDetail: View { Text("The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.") .font(.caption) .foregroundStyle(.red) + Text("Public Key\(user.publicKey?.base64EncodedString() ?? "Empty Key")") + .monospaced() + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + Text("New Key\(user.newPublicKey?.base64EncodedString() ?? "Empty Key")") + .monospaced() + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) } } icon: { Image(systemName: "key.slash.fill") From 01e303d74b10b5d34fe6132054c3d1ae08710baa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 2 Sep 2024 21:35:01 -0700 Subject: [PATCH 179/333] Add a button to drop a pin for a waypoints in maps --- Localizable.xcstrings | 3 +++ .../Nodes/Helpers/Map/WaypointForm.swift | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 7298c16e..fc3ff448 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -6728,6 +6728,9 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { + }, + "Drop Pin in Maps" : { + }, "echo" : { "localizations" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 17573745..99b3b12c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -269,10 +269,8 @@ struct WaypointForm: View { .fixedSize(horizontal: false, vertical: true) } icon: { Image(systemName: "doc.plaintext") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) } - .padding(.bottom, 5) + .padding(.bottom) } /// Coordinate Label { @@ -280,11 +278,18 @@ struct WaypointForm: View { .textSelection(.enabled) .foregroundColor(.primary) } icon: { - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) + Image(systemName: "mappin.circle") } - .padding(.bottom, 5) + .padding(.bottom) + // Drop Maps Pin + Button(action: { + if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") { + UIApplication.shared.open(url) + } + }) { + Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse") + } + .padding(.bottom) /// Created Label { Text("Created: \(waypoint.created?.formatted() ?? "?")") @@ -292,9 +297,8 @@ struct WaypointForm: View { } icon: { Image(systemName: "clock.badge.checkmark") .symbolRenderingMode(.hierarchical) - .frame(width: 35) } - .padding(.bottom, 5) + .padding(.bottom) /// Updated if waypoint.lastUpdated != nil { Label { @@ -303,9 +307,8 @@ struct WaypointForm: View { } icon: { Image(systemName: "clock.arrow.circlepath") .symbolRenderingMode(.hierarchical) - .frame(width: 35) } - .padding(.bottom, 5) + .padding(.bottom) } /// Expires if waypoint.expire != nil { From 3934bdcbc6222a60d2f14956102ac12f133039cf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 2 Sep 2024 22:23:56 -0700 Subject: [PATCH 180/333] Don't set keymatch to false if we get a pki failed error --- Meshtastic/Helpers/MeshPackets.swift | 5 ----- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f46600aa..efd80d72 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -632,11 +632,6 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana } } fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) - if routingError == RoutingError.pkiFailed { - fetchedMessage[0].toUser?.keyMatch = false - fetchedMessage[0].toUser?.newPublicKey = fetchedMessage[0].publicKey - } - if routingMessage.errorReason == Routing.Error.none { fetchedMessage[0].receivedACK = true diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index f6e701a6..9195d068 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -55,11 +55,14 @@ struct NodeDetail: View { .font(.caption) .foregroundStyle(.red) Text("Public Key\(user.publicKey?.base64EncodedString() ?? "Empty Key")") + .font(.caption2) .monospaced() .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) Text("New Key\(user.newPublicKey?.base64EncodedString() ?? "Empty Key")") + .font(.caption2) .monospaced() .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + } } icon: { Image(systemName: "key.slash.fill") From a94fd470e5b6ed9dd55b507653d6658588bfb248 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 2 Sep 2024 22:42:51 -0700 Subject: [PATCH 181/333] Remove the keys again --- Localizable.xcstrings | 9 +++------ Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 15 +++------------ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 7298c16e..d1bbd58b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15389,9 +15389,6 @@ }, "Never" : { - }, - "New Key%@" : { - }, "Newer firmware is available" : { @@ -16759,9 +16756,6 @@ }, "Public Key Mismatch" : { - }, - "Public Key%@" : { - }, "PWD" : { @@ -21128,6 +21122,9 @@ }, "The minimum distance change in meters to be considered for a smart position broadcast." : { + }, + "The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again, but this also may indicate a more serious security problem. Contact the user through another trusted channel to determine if the key change was due to a factory reset or other intentional action." : { + }, "The public key authorized to send admin messages to this node." : { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 9195d068..11defc79 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -51,18 +51,9 @@ struct NodeDetail: View { Text("Public Key Mismatch") .font(.title3) .foregroundStyle(.red) - Text("The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.") - .font(.caption) - .foregroundStyle(.red) - Text("Public Key\(user.publicKey?.base64EncodedString() ?? "Empty Key")") - .font(.caption2) - .monospaced() - .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) - Text("New Key\(user.newPublicKey?.base64EncodedString() ?? "Empty Key")") - .font(.caption2) - .monospaced() - .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) - + Text("The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again, but this also may indicate a more serious security problem. Contact the user through another trusted channel to determine if the key change was due to a factory reset or other intentional action.") + .foregroundStyle(.secondary) + .font(.callout) } } icon: { Image(systemName: "key.slash.fill") From 5fbb4ecec94942b9b1ee0fb22046f753d2fa9600 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 3 Sep 2024 07:12:15 -0700 Subject: [PATCH 182/333] Clean up waypoint form --- Localizable.xcstrings | 14 ++++------- .../Nodes/Helpers/Map/WaypointForm.swift | 23 ++++++++++++++----- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index fc3ff448..b880a514 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4911,15 +4911,8 @@ } } }, - "Coordinates: %@, %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Coordinates: %1$@, %2$@" - } - } - } + "Coordinates:" : { + }, "copy" : { "localizations" : { @@ -10934,6 +10927,9 @@ }, "Location" : { + }, + "Location:" : { + }, "Location: %@" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 99b3b12c..35916eb6 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -28,6 +28,8 @@ struct WaypointForm: View { @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours @State private var locked: Bool = false @State private var lockedTo: Int64 = 0 + @State private var detents: Set = [.medium, .fraction(0.85)] + @State private var selectedDetent: PresentationDetent = .medium var body: some View { NavigationStack { @@ -39,9 +41,12 @@ struct WaypointForm: View { let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude )) Section(header: Text("Coordinate") ) { HStack { - Text("Location: \(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))") + Text("Location:") + .foregroundColor(.secondary) + Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))") .textSelection(.enabled) - .foregroundColor(Color.gray) + .foregroundColor(.secondary) + .font(.caption) } HStack { if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { @@ -124,6 +129,7 @@ struct WaypointForm: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } + .scrollDismissesKeyboard(.immediately) HStack { Button { /// Send a new or exiting waypoint @@ -239,7 +245,7 @@ struct WaypointForm: View { } else { VStack { HStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 65) + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50) Spacer() Text(waypoint.name ?? "?") .font(.largeTitle) @@ -250,6 +256,7 @@ struct WaypointForm: View { } else { Button { editMode = true + selectedDetent = .fraction(0.85) } label: { Image(systemName: "square.and.pencil" ) .font(.largeTitle) @@ -274,9 +281,12 @@ struct WaypointForm: View { } /// Coordinate Label { - Text("Coordinates: \(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))") - .textSelection(.enabled) + Text("Coordinates:") .foregroundColor(.primary) + Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))") + .textSelection(.enabled) + .foregroundColor(.secondary) + .font(.caption2) } icon: { Image(systemName: "mappin.circle") } @@ -381,7 +391,8 @@ struct WaypointForm: View { longitude = waypoint.coordinate.longitude } } - .presentationDetents([.fraction(0.75)]) + .presentationDetents(detents, selection: $selectedDetent) + .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) .presentationDragIndicator(.visible) } } From 9d911a8d2c1360a3e3a25a70c8190df350721d64 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 3 Sep 2024 18:01:32 -0700 Subject: [PATCH 183/333] hook up session passkey to bluetooth config --- Localizable.xcstrings | 3 --- Meshtastic/Helpers/BLEManager.swift | 3 +++ Meshtastic/Helpers/MeshPackets.swift | 4 +++- Meshtastic/Views/Messages/UserList.swift | 2 ++ .../Settings/Config/BluetoothConfig.swift | 23 +++++++++---------- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index df568dc1..1c7ebdc7 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5431,9 +5431,6 @@ }, "Device is managed by a mesh administrator." : { - }, - "Device Logging Enabled" : { - }, "Device Metrics" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 08b53801..87551c4f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2564,6 +2564,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig + if UserDefaults.enableAdministration { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index efd80d72..9f3186db 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -50,6 +50,7 @@ func generateMessageMarkdown (message: String) -> String { func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { + let remote = nodeNum != UserDefaults.preferredPeripheralNum if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { @@ -496,7 +497,8 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { let config = adminMessage.getConfigResponse if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { - upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), context: context) + upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + /// upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 3a2c4a47..44b92621 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -46,6 +46,7 @@ struct UserList: View { NSSortDescriptor(key: "userNode.favorite", ascending: false), NSSortDescriptor(key: "pkiEncrypted", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], + predicate: NSPredicate(format: "hwModelId != nil"), animation: .default ) private var users: FetchedResults @@ -95,6 +96,7 @@ struct UserList: View { Text(user.longName ?? "unknown".localized) .font(.headline) .allowsTightening(true) + Text(user.hwModel ?? "") Spacer() if user.userNode?.favorite ?? false { Image(systemName: "star.fill") diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 75132df1..2ec96942 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -19,7 +19,6 @@ struct BluetoothConfig: View { @State var mode = 0 @State var fixedPin = "123456" @State var shortPin = false - @State var deviceLoggingEnabled = false var pinLength: Int = 6 let numberFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -70,10 +69,6 @@ struct BluetoothConfig: View { .foregroundColor(.red) } } - Toggle(isOn: $deviceLoggingEnabled) { - Label("Device Logging Enabled", systemImage: "ladybug") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } .disabled(self.bleManager.connectedPeripheral == nil || node?.bluetoothConfig == nil) @@ -85,7 +80,6 @@ struct BluetoothConfig: View { bc.enabled = enabled bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin bc.fixedPin = UInt32(fixedPin) ?? 123456 - bc.deviceLoggingEnabled = deviceLoggingEnabled let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -106,13 +100,22 @@ struct BluetoothConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a BluetoothConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node, node.bluetoothConfig == nil { Logger.mesh.info("empty bluetooth config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.bluetoothConfig == nil { + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } @@ -125,15 +128,11 @@ struct BluetoothConfig: View { .onChange(of: fixedPin) { newFixedPin in if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true } } - .onChange(of: deviceLoggingEnabled) { - if $0 != node?.bluetoothConfig?.deviceLoggingEnabled { hasChanges = true } - } } func setBluetoothValues() { self.enabled = node?.bluetoothConfig?.enabled ?? true self.mode = Int(node?.bluetoothConfig?.mode ?? 0) self.fixedPin = String(node?.bluetoothConfig?.fixedPin ?? 123456) - self.deviceLoggingEnabled = node?.bluetoothConfig?.deviceLoggingEnabled ?? false self.hasChanges = false } } From 359dbacb44d8fad8713948f43a4e33912ab80e12 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:50:08 -0700 Subject: [PATCH 184/333] Actionable Notifs --- .../xcshareddata/swiftpm/Package.resolved | 4 +- Meshtastic/Helpers/BLEManager.swift | 12 ++-- .../Helpers/LocalNotificationManager.swift | 33 +++++++++ Meshtastic/Helpers/MeshPackets.swift | 44 +++++++----- Meshtastic/MeshtasticAppDelegate.swift | 68 +++++++++++++++++-- 5 files changed, 132 insertions(+), 29 deletions(-) diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index c47a9058..3465793d 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "d57a5aecf24a25b32ec4a74be2f5d0a995a47c4b", - "version" : "1.27.0" + "revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5", + "version" : "1.28.1" } } ], diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 08b53801..71e78fc8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -3299,7 +3299,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MARK: - CB Central Manager implmentation extension BLEManager: CBCentralManagerDelegate { - + // MARK: Bluetooth enabled/disabled func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == CBManagerState.poweredOn { @@ -3309,9 +3309,9 @@ extension BLEManager: CBCentralManagerDelegate { } else { isSwitchedOn = false } - + var status = "" - + switch central.state { case .poweredOff: status = "BLE is powered off" @@ -3330,10 +3330,10 @@ extension BLEManager: CBCentralManagerDelegate { } Logger.services.info("📜 [BLE] Bluetooth status: \(status)") } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - + if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) Logger.services.info("✅ [BLE] Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown", privacy: .public)") @@ -3341,7 +3341,7 @@ extension BLEManager: CBCentralManagerDelegate { let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral) - + if let peripheralIndex = index { peripherals[peripheralIndex] = device } else { diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 3e41c8f3..1260f20c 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -5,6 +5,16 @@ import OSLog class LocalNotificationManager { var notifications = [Notification]() + + let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: + "👍 \(Tapbacks.thumbsUp.description)", options: []) + let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: + "👎 \(Tapbacks.thumbsDown.description)", options: []) + let replyInputAction = UNTextInputNotificationAction( + identifier: "messageNotification.replyInputAction", + title: "reply".localized, + options: []) + // Step 1 Request Permissions for notifications private func requestAuthorization() { @@ -31,6 +41,15 @@ class LocalNotificationManager { // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future private func scheduleNotifications() { + let messageNotificationCategory = UNNotificationCategory( + identifier: "messageNotificationCategory", + actions: [thumbsUpAction, thumbsDownAction,replyInputAction], + intentIdentifiers: [], + options: .customDismissAction + ) + + UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) + for notification in notifications { let content = UNMutableNotificationContent() content.subtitle = notification.subtitle @@ -45,6 +64,17 @@ class LocalNotificationManager { if notification.path != nil { content.userInfo["path"] = notification.path } + if notification.messageId != nil { + content.categoryIdentifier = "messageNotificationCategory" + content.userInfo["messageId"] = notification.messageId + } + if notification.channel != nil { + content.userInfo["channel"] = notification.channel + } + if notification.userNum != nil { + content.userInfo["userNum"] = notification.userNum + } + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) @@ -76,4 +106,7 @@ struct Notification { var content: String var target: String? var path: String? + var messageId: Int64? + var channel: Int32? + var userNum: Int64? } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index efd80d72..a41fec2d 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -815,12 +815,10 @@ func textMessageAppPacket( context: NSManagedObjectContext, appState: AppState ) { - var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) let rangeRef = Reference(Int.self) let rangeTestRegex = Regex { "seq " - TryCapture(as: rangeRef) { OneOrMore(.digit) } transform: { match in @@ -828,7 +826,7 @@ func textMessageAppPacket( } } let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false - + if !wantRangeTestPackets && rangeTest { return } @@ -841,15 +839,16 @@ func textMessageAppPacket( } } } - + if messageText?.count ?? 0 > 0 { - MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") - + let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) + do { let fetchedUsers = try context.fetch(messageUsers) + let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) if packet.rxTime > 0 { @@ -865,54 +864,60 @@ func textMessageAppPacket( newMessage.portNum = Int32(packet.decoded.portnum.rawValue) newMessage.publicKey = packet.publicKey newMessage.pkiEncrypted = packet.pkiEncrypted + if packet.decoded.portnum == PortNum.detectionSensorApp { if !UserDefaults.enableDetectionNotifications { newMessage.read = true } } + if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } - + if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum { if !storeForwardBroadcast { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) } } + if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) + if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) { - /// We have a key, check if it matches + // We have a key, check if it matches if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false newMessage.fromUser?.newPublicKey = newMessage.publicKey } } else { - /// We have no key, set it + // We have no key, set it newMessage.fromUser?.publicKey = packet.publicKey newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted } + if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } else { newMessage.fromUser?.userNode?.lastHeard = Date() } } + newMessage.messagePayload = messageText newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!) + if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil { newMessage.fromUser?.lastMessage = Date() } + var messageSaved = false - + do { - try context.save() Logger.data.info("💾 Saved a new message for \(newMessage.messageId)") messageSaved = true if messageSaved { - if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { return } @@ -931,14 +936,16 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)" + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(packet.from) ) ] manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") } } else if newMessage.fromUser != nil && newMessage.toUser == nil { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) @@ -961,7 +968,11 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)") + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0") + ) ] manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") @@ -969,7 +980,7 @@ func textMessageAppPacket( } } } catch { - + // Handle error } } } @@ -984,6 +995,7 @@ func textMessageAppPacket( } } + func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 06d290e9..d38557bc 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -9,9 +9,9 @@ import SwiftUI import OSLog class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { - + var router: Router? - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Logger.services.info("🚀 [App] Meshtstic Apple App launched!") // Default User Default Values @@ -22,7 +22,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if #available(iOS 17.0, macOS 14.0, *) { let locationsHandler = LocationsHandler.shared locationsHandler.startLocationUpdates() - + // If a background activity session was previously active, reinstantiate it after the background launch. if locationsHandler.backgroundActivity { locationsHandler.backgroundActivity = true @@ -38,7 +38,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat ) { completionHandler([.list, .banner, .sound]) } - + // This method is called when a user clicks on the notification func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -46,6 +46,64 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat withCompletionHandler completionHandler: @escaping () -> Void ) { let userInfo = response.notification.request.content.userInfo + + switch response.actionIdentifier { + case UNNotificationDefaultActionIdentifier: + break + + case "messageNotification.thumbsUpAction": + if let channel = userInfo["channel"] as? Int32, + let replyID = userInfo["messageId"] as? Int64 { + let tapbackResponse = !BLEManager.shared.sendMessage( + message: Tapbacks.thumbsUp.emojiString, + toUserNum: userInfo["userNum"] as? Int64 ?? 0, + channel: channel, + isEmoji: true, + replyID: replyID + ) + Logger.services.info("Tapback response sent") + } else { + Logger.services.error("Failed to retrieve channel or messageId from userInfo") + } + break + + case "messageNotification.thumbsDownAction": + if let channel = userInfo["channel"] as? Int32, + let replyID = userInfo["messageId"] as? Int64 { + let tapbackResponse = !BLEManager.shared.sendMessage( + message: Tapbacks.thumbsDown.emojiString, + toUserNum: userInfo["userNum"] as? Int64 ?? 0, + channel: channel, + isEmoji: true, + replyID: replyID + ) + Logger.services.info("Tapback response sent") + } else { + Logger.services.error("Failed to retrieve channel or messageId from userInfo") + } + break + + case "messageNotification.replyInputAction": + if let userInput = (response as? UNTextInputNotificationResponse)?.userText, + let channel = userInfo["channel"] as? Int32, + let replyID = userInfo["messageId"] as? Int64 { + let tapbackResponse = !BLEManager.shared.sendMessage( + message: userInput, + toUserNum: userInfo["userNum"] as? Int64 ?? 0, + channel: channel, + isEmoji: false, + replyID: replyID + ) + Logger.services.info("Actionable notification reply sent") + } else { + Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo") + } + break + + default: + break + } + if let targetValue = userInfo["target"] as? String, let deepLink = userInfo["path"] as? String, let url = URL(string: deepLink) { @@ -54,7 +112,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to handle notification response: \(userInfo)") } - + completionHandler() } } From a4fe551e039edc29d7cc2eb740dfd6dd313d76a4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 4 Sep 2024 10:06:34 -0700 Subject: [PATCH 185/333] Update several config sections to use the new pki admin structures --- Localizable.xcstrings | 22 ++------ Meshtastic/Extensions/View.swift | 4 +- Meshtastic/Helpers/MeshPackets.swift | 13 ++--- Meshtastic/Persistence/UpdateCoreData.swift | 2 - .../Settings/Config/BluetoothConfig.swift | 2 +- .../Views/Settings/Config/ConfigHeader.swift | 5 +- .../Views/Settings/Config/DeviceConfig.swift | 21 +++++-- .../Views/Settings/Config/DisplayConfig.swift | 21 +++++-- .../Views/Settings/Config/LoRaConfig.swift | 19 +++++-- .../Settings/Config/SecurityConfig.swift | 55 +++++++++---------- 10 files changed, 88 insertions(+), 76 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1c7ebdc7..1857e7d1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -109,7 +109,7 @@ "%@ Channels?" : { }, - "%@ config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log." : { + "%@ config data was requested over the admin channel but no response has been returned from the remote node." : { }, "%@ dB" : { @@ -1865,9 +1865,6 @@ } } } - }, - "Bluetooth Logs" : { - }, "bluetooth.config" : { "localizations" : { @@ -5070,6 +5067,9 @@ }, "Debug Log" : { + }, + "Debug Logs" : { + }, "Debug Logs%@" : { @@ -5361,9 +5361,6 @@ } } } - }, - "Developer" : { - }, "Developers" : { @@ -16073,7 +16070,7 @@ "Other data sources" : { }, - "Output live debug logging over serial." : { + "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth." : { }, "Output pin buzzer GPIO " : { @@ -19273,9 +19270,6 @@ }, "Serial Console over the Stream API." : { - }, - "Serial Debug Logs" : { - }, "serial.config" : { "localizations" : { @@ -22508,12 +22502,6 @@ }, "Via Mqtt" : { - }, - "View and export position-redacted device logs over Bluetooth" : { - - }, - "View Logs" : { - }, "voltage" : { "localizations" : { diff --git a/Meshtastic/Extensions/View.swift b/Meshtastic/Extensions/View.swift index 7349f36d..cec5b003 100644 --- a/Meshtastic/Extensions/View.swift +++ b/Meshtastic/Extensions/View.swift @@ -8,13 +8,13 @@ import SwiftUI public extension View { - func onFirstAppear(_ action: @escaping () -> ()) -> some View { + func onFirstAppear(_ action: @escaping () -> Void) -> some View { modifier(FirstAppear(action: action)) } } private struct FirstAppear: ViewModifier { - let action: () -> () + let action: () -> Void // Use this to only fire your block one time @State private var hasAppeared = false diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 9f3186db..261ffcf2 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -498,19 +498,18 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { let config = adminMessage.getConfigResponse if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - /// upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { - upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), context: context) + upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { - upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), context: context) + upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), context: context) + upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { - upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), context: context) + upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { - upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), context: context) + upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { - upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), context: context) + upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 7c43448b..b3e4dba6 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -409,13 +409,11 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, newBluetoothConfig.enabled = config.enabled newBluetoothConfig.mode = Int32(config.mode.rawValue) newBluetoothConfig.fixedPin = Int32(config.fixedPin) - newBluetoothConfig.deviceLoggingEnabled = config.deviceLoggingEnabled fetchedNode[0].bluetoothConfig = newBluetoothConfig } else { fetchedNode[0].bluetoothConfig?.enabled = config.enabled fetchedNode[0].bluetoothConfig?.mode = Int32(config.mode.rawValue) fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin) - fetchedNode[0].bluetoothConfig?.deviceLoggingEnabled = config.deviceLoggingEnabled } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 2ec96942..1fdd3a89 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -102,7 +102,7 @@ struct BluetoothConfig: View { ) .onFirstAppear { // Need to request a BluetoothConfig from the remote node before allowing changes - if let connectedPeripheral = bleManager.connectedPeripheral, let node, node.bluetoothConfig == nil { + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty bluetooth config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index 3f59a01a..d1af3a6a 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -17,8 +17,9 @@ struct ConfigHeader: View { } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { // Let users know what is going on if they are using remote admin and don't have the config yet - if node?[keyPath: config] == nil { - Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + let expiration = node?.sessionExpiration ?? Date() + if node?[keyPath: config] == nil || expiration < node?.sessionExpiration ?? Date() { + Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node.") .font(.callout) .foregroundColor(.orange) } else { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 13d3aa83..ea01e66f 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -242,13 +242,24 @@ struct DeviceConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a DeviceConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil { + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty device config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) - if node != nil && connectedNode != nil && connectedNode?.user != nil { - _ = bleManager.requestDeviceConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.deviceConfig == nil { + _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + if node.deviceConfig == nil { + /// Legacy Administration + _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 1f3f6de4..0ef44bc1 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -163,13 +163,22 @@ struct DisplayConfig: View { ) } ) - .onAppear { - // Need to request a LoRaConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.displayConfig == nil { + .onFirstAppear { + // Need to request a DisplayConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty display config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestDisplayConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.displayConfig == nil { + _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index dad7c1a4..b9926b8a 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -232,13 +232,22 @@ struct LoRaConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a LoRaConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil { + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty lora config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.loRaConfig == nil { + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e07ed945..5576ac6f 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -30,7 +30,6 @@ struct SecurityConfig: View { @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false - @State var bluetoothLoggingEnabled = false @State var adminChannelEnabled = false var body: some View { @@ -71,10 +70,15 @@ struct SecurityConfig: View { } } Section(header: Text("Logs")) { - Toggle(isOn: $bluetoothLoggingEnabled) { - Label("Bluetooth Logs", systemImage: "dot.radiowaves.right") - Text("View and export position-redacted device logs over Bluetooth") - Link("View Logs", destination: URL(string: "meshtastic:///settings/debugLogs")!) + Toggle(isOn: $serialEnabled) { + Label("Serial Console", systemImage: "terminal") + Text("Serial Console over the Stream API.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + Toggle(isOn: $debugLogApiEnabled) { + Label("Debug Logs", systemImage: "ant.fill") + Text("Output live debug logging over serial, view and export position-redacted device logs over Bluetooth.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } @@ -92,20 +96,6 @@ struct SecurityConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Section(header: Text("Developer")) { - Toggle(isOn: $serialEnabled) { - Label("Serial Console", systemImage: "terminal") - Text("Serial Console over the Stream API.") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if serialEnabled { - Toggle(isOn: $debugLogApiEnabled) { - Label("Serial Debug Logs", systemImage: "ant.fill") - Text("Output live debug logging over serial.") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - } } } .scrollDismissesKeyboard(.immediately) @@ -126,9 +116,6 @@ struct SecurityConfig: View { .onChange(of: debugLogApiEnabled) { if $0 != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } } - .onChange(of: bluetoothLoggingEnabled) { - if $0 != node?.securityConfig?.bluetoothLoggingEnabled { hasChanges = true } - } .onChange(of: adminChannelEnabled) { if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } @@ -162,11 +149,23 @@ struct SecurityConfig: View { hasChanges = true } .onFirstAppear { - // Need to request a Power config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.securityConfig == nil { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestSecurityConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + // Need to request a DeviceConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty security config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.securityConfig == nil { + _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + if node.deviceConfig == nil { + /// Legacy Administration + _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } @@ -190,7 +189,6 @@ struct SecurityConfig: View { config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled - config.bluetoothLoggingEnabled = bluetoothLoggingEnabled config.adminChannelEnabled = adminChannelEnabled let adminMessageId = bleManager.saveSecurityConfig( @@ -215,7 +213,6 @@ struct SecurityConfig: View { self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false - self.bluetoothLoggingEnabled = node?.securityConfig?.bluetoothLoggingEnabled ?? false self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false self.hasChanges = false } From 44d65d8185dc50d66f860df673b89573c36cb687 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 4 Sep 2024 10:27:06 -0700 Subject: [PATCH 186/333] Additional config sections --- Meshtastic.xcodeproj/project.pbxproj | 2 +- .../Views/Settings/Config/NetworkConfig.swift | 19 +++++++++++ .../Settings/Config/PositionConfig.swift | 23 +++++++------ .../Views/Settings/Config/PowerConfig.swift | 32 ++++++++++++++++--- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 83fac71b..43f27393 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -719,8 +719,8 @@ DD41582528582E9B009B0E59 /* DeviceConfig.swift */, DD8EBF42285058FA00426DCA /* DisplayConfig.swift */, DD2553562855B02500E55709 /* LoRaConfig.swift */, - DD2553582855B52700E55709 /* PositionConfig.swift */, DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */, + DD2553582855B52700E55709 /* PositionConfig.swift */, D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */, DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */, DD61937B2863877A00E59241 /* Module */, diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 0554c766..dd767b6c 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -128,6 +128,25 @@ struct NetworkConfig: View { } } } + .onFirstAppear { + // Need to request a NetworkConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty network config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.networkConfig == nil { + _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } + } + } .onChange(of: wifiEnabled) { if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 0f71b379..392486da 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -376,18 +376,23 @@ struct PositionConfig: View { ) } ) - .onAppear { + .onFirstAppear { supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame - // Need to request a PositionConfig from the remote node before allowing changes - if let connectedPeripheral = bleManager.connectedPeripheral, node?.positionConfig == nil { + // Need to request a NetworkConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty position config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) - if let node, let connectedNode { - _ = bleManager.requestPositionConfig( - fromUser: connectedNode.user!, - toUser: node.user!, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 - ) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.positionConfig == nil { + _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 42b6a534..cf213f8e 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -1,5 +1,6 @@ import SwiftUI import MeshtasticProtobufs +import OSLog struct PowerConfig: View { @Environment(\.managedObjectContext) private var context @@ -118,6 +119,17 @@ struct PowerConfig: View { } } .onAppear { + + // Need to request a Power config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.powerConfig == nil { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestPowerConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } + + .onFirstAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" @@ -127,11 +139,21 @@ struct PowerConfig: View { } } } - // Need to request a Power config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.powerConfig == nil { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestPowerConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + // Need to request a NetworkConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty power config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.positionConfig == nil { + _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } From e294a2480c59120853ac49f44658c738d5b5928a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 4 Sep 2024 12:22:25 -0700 Subject: [PATCH 187/333] Remove debugging hardware version --- Meshtastic/Views/Messages/UserList.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 44b92621..4e09859f 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -96,7 +96,6 @@ struct UserList: View { Text(user.longName ?? "unknown".localized) .font(.headline) .allowsTightening(true) - Text(user.hwModel ?? "") Spacer() if user.userNode?.favorite ?? false { Image(systemName: "star.fill") From 23d62e8e1237e4e0a52e44f5c26519b2c31ff65f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 4 Sep 2024 18:02:17 -0700 Subject: [PATCH 188/333] Set public key on upsert of existing node --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index b3e4dba6..f58a0a64 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -269,6 +269,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) 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!.pkiEncrypted = packet.pkiEncrypted + fetchedNode[0].user!.publicKey = packet.publicKey Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) From 1853afde73e29e7c2381b4ae63af28f596f61866 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 4 Sep 2024 18:13:35 -0700 Subject: [PATCH 189/333] allow retry of pki failed --- Meshtastic/Enums/RoutingError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 108fedd2..8b50de89 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -95,7 +95,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .notAuthorized: return true case .pkiFailed: - return false + return true case .pkiUnknownPubkey: return false } From 6e6027263a2ef9a9bbd02fa68cf75254fab1e1a0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 5 Sep 2024 10:39:54 -0700 Subject: [PATCH 190/333] Log node info packet json --- Localizable.xcstrings | 5 +---- Meshtastic/Helpers/BLEManager.swift | 1 + Meshtastic/Helpers/MeshPackets.swift | 2 +- Meshtastic/Persistence/UpdateCoreData.swift | 7 +++++-- .../Views/Settings/Config/DeviceConfig.swift | 16 ---------------- .../Views/Settings/Config/SecurityConfig.swift | 3 +-- 6 files changed, 9 insertions(+), 25 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 1857e7d1..b6673742 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5426,7 +5426,7 @@ "Device GPS" : { }, - "Device is managed by a mesh administrator." : { + "Device is managed by a mesh administrator, the user is unable to access any of the device settings." : { }, "Device Metrics" : { @@ -6923,9 +6923,6 @@ }, "Enabling Ethernet will disable the bluetooth connection to the app." : { - }, - "Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it." : { - }, "Enabling WiFi will disable the bluetooth connection to the app." : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 87551c4f..11597647 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -766,6 +766,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .waypointApp: waypointPacket(packet: decodedInfo.packet, context: context) case .nodeinfoApp: + MeshLogger.log("🕸️ MESH PACKET received for Node Info App for PKI LOCK debugging \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral.num, context: context) } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 261ffcf2..3b6b2b65 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -366,7 +366,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user = UserEntity(context: context) } // Set the public key for a user if it is empty, don't update - if fetchedNode[0].user?.publicKey?.isEmpty == nil && !nodeInfo.user.publicKey.isEmpty { + if fetchedNode[0].user?.publicKey == nil && !nodeInfo.user.publicKey.isEmpty { fetchedNode[0].user?.pkiEncrypted = true fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index f58a0a64..039b654e 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -269,8 +269,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) 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!.pkiEncrypted = packet.pkiEncrypted - fetchedNode[0].user!.publicKey = packet.publicKey + + if !packet.publicKey.isEmpty { + fetchedNode[0].user!.pkiEncrypted = packet.pkiEncrypted + fetchedNode[0].user!.publicKey = packet.publicKey + } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index ea01e66f..9382b297 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -29,7 +29,6 @@ struct DeviceConfig: View { @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @State var ledHeartbeatEnabled = true - @State var isManaged = false @State var tzdef = "" var body: some View { @@ -62,12 +61,6 @@ struct DeviceConfig: View { } .pickerStyle(DefaultPickerStyle()) - Toggle(isOn: $isManaged) { - Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") - Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) { ForEach(UpdateIntervals.allCases) { ui in if ui.rawValue >= 3600 { @@ -213,13 +206,8 @@ struct DeviceConfig: View { dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress - dc.isManaged = isManaged dc.tzdef = tzdef dc.ledHeartbeatDisabled = !ledHeartbeatEnabled - if isManaged { - serialEnabled = false - debugLogEnabled = false - } let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -287,9 +275,6 @@ struct DeviceConfig: View { .onChange(of: doubleTapAsButtonPress) { if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } } - .onChange(of: isManaged) { - if $0 != node?.deviceConfig?.isManaged { hasChanges = true } - } .onChange(of: tzdef) { newTzdef in if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } } @@ -312,7 +297,6 @@ struct DeviceConfig: View { } self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true - self.isManaged = node?.deviceConfig?.isManaged ?? false self.tzdef = node?.deviceConfig?.tzdef ?? "" if self.tzdef.isEmpty { self.tzdef = TimeZone.current.posixDescription diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 5576ac6f..6388ea6e 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -75,7 +75,6 @@ struct SecurityConfig: View { Text("Serial Console over the Stream API.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $debugLogApiEnabled) { Label("Debug Logs", systemImage: "ant.fill") Text("Output live debug logging over serial, view and export position-redacted device logs over Bluetooth.") @@ -86,7 +85,7 @@ struct SecurityConfig: View { if adminKey.length > 0 || adminChannelEnabled { Toggle(isOn: $isManaged) { Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") - Text("Device is managed by a mesh administrator.") + Text("Device is managed by a mesh administrator, the user is unable to access any of the device settings.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } From b32cabec54e7dba50cdbc947570b57f0f5b1601d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 5 Sep 2024 13:42:46 -0700 Subject: [PATCH 191/333] set keys on create user catch --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 039b654e..624dbb20 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -209,6 +209,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { if packet.from > Constants.minimumNodeNum { let newUser = createUser(num: Int64(packet.from), context: context) + newNode.user?.pkiEncrypted = packet.pkiEncrypted + newNode.user?.publicKey = packet.publicKey newNode.user = newUser } } From d95cf2d1901e45151d0fdbabef764c82511f9f72 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 5 Sep 2024 14:07:40 -0700 Subject: [PATCH 192/333] Add lock to node list --- Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 63aff338..481666a1 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -30,8 +30,22 @@ struct NodeListItem: View { } VStack(alignment: .leading) { HStack { + if node.user?.pkiEncrypted ?? false { + if !(node.user?.keyMatch ?? false) { + /// 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.open.fill") + .foregroundColor(.yellow) + } Text(node.user?.longName ?? "unknown".localized) .font(.headline) + .fontWeight(.regular) .allowsTightening(true) if node.favorite { Spacer() From d0e10ef3301b5485c8210398d44b433b705c33a4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 5 Sep 2024 19:31:29 -0700 Subject: [PATCH 193/333] Finish up the admin, make the admin dropdown a navigationlink so you can read the full long name --- Localizable.xcstrings | 3 -- Meshtastic/Views/Settings/Channels.swift | 9 +++--- .../Views/Settings/Channels/ChannelForm.swift | 26 ++++++++--------- .../Settings/Config/BluetoothConfig.swift | 16 +++++----- .../Views/Settings/Config/DeviceConfig.swift | 22 +++++++------- .../Views/Settings/Config/DisplayConfig.swift | 16 +++++----- .../Views/Settings/Config/LoRaConfig.swift | 16 +++++----- .../Config/Module/AmbientLightingConfig.swift | 23 +++++++++++---- .../Config/Module/CannedMessagesConfig.swift | 23 +++++++++++---- .../Config/Module/DetectionSensorConfig.swift | 25 +++++++++++----- .../Module/ExternalNotificationConfig.swift | 27 ++++++++++++----- .../Settings/Config/Module/MQTTConfig.swift | 23 +++++++++++---- .../Config/Module/PaxCounterConfig.swift | 25 ++++++++++++---- .../Config/Module/RangeTestConfig.swift | 25 +++++++++++----- .../Settings/Config/Module/RtttlConfig.swift | 25 ++++++++++++---- .../Settings/Config/Module/SerialConfig.swift | 25 +++++++++++----- .../Config/Module/StoreForwardConfig.swift | 25 +++++++++++----- .../Config/Module/TelemetryConfig.swift | 21 ++++++++++---- .../Views/Settings/Config/NetworkConfig.swift | 16 +++++----- .../Settings/Config/PositionConfig.swift | 16 +++++----- .../Views/Settings/Config/PowerConfig.swift | 29 +++++++------------ .../Settings/Config/SecurityConfig.swift | 22 +++++++------- Meshtastic/Views/Settings/Settings.swift | 6 ++-- 23 files changed, 296 insertions(+), 168 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b6673742..05c2bbb8 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4581,9 +4581,6 @@ }, "Configure" : { - }, - "Configuring Node" : { - }, "Connect to a Node" : { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 61cddcea..f039534c 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -95,7 +95,6 @@ struct Channels: View { if channelKey == "AQ==" { positionPrecision = 13 preciseLocation = false - positionsEnabled = true } } else if !supportedVersion && channelRole == 2 { positionPrecision = 0 @@ -104,7 +103,7 @@ struct Channels: View { } else { if channelKey == "AQ==" { preciseLocation = false - if positionPrecision < 10 || positionPrecision > 16 { + if (positionPrecision > 0 && positionPrecision < 10) || positionPrecision > 16 { positionPrecision = 13 } } else if positionPrecision == 32 { @@ -226,7 +225,7 @@ struct Channels: View { } label: { Label("save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !hasValidKey) + .disabled(bleManager.connectedPeripheral == nil)// || !hasChanges)// !hasValidKey) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) @@ -259,10 +258,9 @@ struct Channels: View { channelKey = key positionsEnabled = false preciseLocation = false - positionPrecision = 13 + positionPrecision = 0 uplink = false downlink = false - hasChanges = true let newChannel = ChannelEntity(context: context) newChannel.id = channelIndex @@ -274,6 +272,7 @@ struct Channels: View { newChannel.psk = Data(base64Encoded: channelKey) ?? Data() newChannel.positionPrecision = Int32(positionPrecision) selectedChannel = newChannel + hasChanges = true } label: { Label("Add Channel", systemImage: "plus.square") diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index d0a111e5..d21e0f42 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -130,7 +130,6 @@ struct ChannelForm: View { } Section(header: Text("position")) { - VStack(alignment: .leading) { Toggle(isOn: $positionsEnabled) { Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash") @@ -140,20 +139,21 @@ struct ChannelForm: View { } if positionsEnabled { - VStack(alignment: .leading) { - Toggle(isOn: $preciseLocation) { - Label("Precise Location", systemImage: "scope") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .disabled(!supportedVersion) - .listRowSeparator(.visible) - .onChange(of: preciseLocation) { pl in - if pl == false { - positionPrecision = 13 + if channelKey != "AQ==" && channelRole > 0 { + VStack(alignment: .leading) { + Toggle(isOn: $preciseLocation) { + Label("Precise Location", systemImage: "scope") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .disabled(!supportedVersion) + .listRowSeparator(.visible) + .onChange(of: preciseLocation) { pl in + if pl == false { + positionPrecision = 13 + } } } } - if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") @@ -213,7 +213,7 @@ struct ChannelForm: View { } .onChange(of: preciseLocation) { loc in if loc == true { - if channelKey == "AQ==" && channelRole == 0 { + if channelKey == "AQ==" { preciseLocation = false } else { positionPrecision = 32 diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 1fdd3a89..46211dac 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -106,15 +106,17 @@ struct BluetoothConfig: View { Logger.mesh.info("empty bluetooth config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.bluetoothConfig == nil { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.bluetoothConfig == nil { + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } - } else { - /// Legacy Administration - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 9382b297..dc17446e 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -236,16 +236,18 @@ struct DeviceConfig: View { Logger.mesh.info("empty device config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.deviceConfig == nil { - _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) - } - } else { - if node.deviceConfig == nil { - /// Legacy Administration - _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.deviceConfig == nil { + _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + if node.deviceConfig == nil { + /// Legacy Administration + _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 0ef44bc1..1e9781a6 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -169,15 +169,17 @@ struct DisplayConfig: View { Logger.mesh.info("empty display config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.displayConfig == nil { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.displayConfig == nil { + _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } - } else { - /// Legacy Administration - _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index b9926b8a..43729f86 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -238,15 +238,17 @@ struct LoRaConfig: View { Logger.mesh.info("empty lora config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.loRaConfig == nil { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.loRaConfig == nil { + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } - } else { - /// Legacy Administration - _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 76c98752..fe3faa51 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -6,6 +6,7 @@ // import MeshtasticProtobufs import SwiftUI +import OSLog @available(iOS 17.0, macOS 14.0, *) struct AmbientLightingConfig: View { @@ -85,12 +86,24 @@ struct AmbientLightingConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a Ambient Lighting Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty ambient lighting config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.ambientLightingConfig == nil { + _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index b4ec0b63..5b94e8c3 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -233,13 +233,24 @@ struct CannedMessagesConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a CannedMessagesModuleConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.cannedMessageConfig == nil { - Logger.mesh.info("empty canned messages module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty canned message config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.cannedMessageConfig == nil { + _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 1ff1ed86..e67898eb 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -189,13 +189,24 @@ struct DetectionSensorConfig: View { ) } ) - .onAppear { - // Need to request a Detection Sensor Module Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { - Logger.mesh.info("empty detection sensor module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .onFirstAppear { + // Need to request a DetectionSensorModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty detection sensor config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.detectionSensorConfig == nil { + _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 32cacbd3..57c3a672 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -199,13 +199,24 @@ struct ExternalNotificationConfig: View { ) } ) - .onAppear { - // Need to request a TelemetryModuleConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.externalNotificationConfig == nil { - Logger.mesh.info("empty external notification module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .onFirstAppear { + // Need to request a ExternalNotificationModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty external notificaiton module config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.externalNotificationConfig == nil { + _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } @@ -279,7 +290,7 @@ struct ExternalNotificationConfig: View { if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } } } - .onChange(of: useI2SAsBuzzer) { + .onChange(of: useI2SAsBuzzer) { if let val = node?.externalNotificationConfig?.useI2SAsBuzzer { hasChanges = $0 != val } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 8ef411ea..d6cbbf2b 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -347,13 +347,24 @@ struct MQTTConfig: View { if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true } } } - .onAppear { - // Need to request a TelemetryModuleConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { + .onFirstAppear { + // Need to request a MqttModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty mqtt module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.mqttConfig == nil { + _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 6e7bef08..1141bc7d 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -7,6 +7,7 @@ import MeshtasticProtobufs import SwiftUI +import OSLog struct PaxCounterConfig: View { @Environment(\.managedObjectContext) private var context @@ -57,12 +58,24 @@ struct PaxCounterConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) - .onAppear { - // Need to request a PAX Counter module config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.paxCounterConfig == nil { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .onFirstAppear { + // Need to request a PaxCounterModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty pax counter module config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.paxCounterConfig == nil { + _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 001ad2b1..872294d8 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -81,13 +81,24 @@ struct RangeTestConfig: View { ) } ) - .onAppear { - // Need to request a RangeTestModule Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { - Logger.mesh.debug("empty range test module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .onFirstAppear { + // Need to request a RangeTestModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty range test module config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.rangeTestConfig == nil { + _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 95f237a3..46b02848 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog struct RtttlConfig: View { @Environment(\.managedObjectContext) var context @@ -71,12 +72,24 @@ struct RtttlConfig: View { ) } ) - .onAppear { - // Need to request a Rtttl Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .onFirstAppear { + // Need to request a RtttlConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty range test module config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.rtttlConfig == nil { + _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index c7243a87..893ddca1 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -136,20 +136,31 @@ struct SerialConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a SerialModuleConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.serialConfig == nil { - Logger.mesh.debug("empty serial module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty serial module config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.serialConfig == nil { + _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } .onChange(of: enabled) { if $0 != node?.serialConfig?.enabled { hasChanges = true } } - .onChange(of: echo) { + .onChange(of: echo) { if $0 != node?.serialConfig?.echo { hasChanges = true } } .onChange(of: rxd) { newRxd in diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 4829a5fa..8fff8979 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -146,13 +146,24 @@ struct StoreForwardConfig: View { ) } ) - .onAppear { - // Need to request a Detection Sensor Module Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil { - Logger.mesh.debug("empty store and forward module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .onFirstAppear { + // Need to request a StoreForwardModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + Logger.mesh.info("empty store & forward module config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.storeForwardConfig == nil { + _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index df811677..afef5727 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -134,13 +134,24 @@ struct TelemetryConfig: View { ) } ) - .onAppear { + .onFirstAppear { // Need to request a TelemetryModuleConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil { + if let connectedPeripheral = bleManager.connectedPeripheral, let node { Logger.mesh.info("empty telemetry module config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration && node.num != connectedNode.num { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.telemetryConfig == nil { + _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } } } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index dd767b6c..928bad57 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -134,15 +134,17 @@ struct NetworkConfig: View { Logger.mesh.info("empty network config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.networkConfig == nil { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.networkConfig == nil { + _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } - } else { - /// Legacy Administration - _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 392486da..4497c1f7 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -383,15 +383,17 @@ struct PositionConfig: View { Logger.mesh.info("empty position config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.positionConfig == nil { + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.positionConfig == nil { + _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } - } else { - /// Legacy Administration - _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index cf213f8e..9ba0fe0a 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -118,17 +118,6 @@ struct PowerConfig: View { .font(.subheadline) } } - .onAppear { - - // Need to request a Power config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.powerConfig == nil { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestPowerConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) - } - } - } - .onFirstAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -144,15 +133,17 @@ struct PowerConfig: View { Logger.mesh.info("empty power config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.positionConfig == nil { - _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.powerConfig == nil { + _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + /// Legacy Administration + _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } - } else { - /// Legacy Administration - _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 6388ea6e..e58de058 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -153,16 +153,18 @@ struct SecurityConfig: View { Logger.mesh.info("empty security config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { - if UserDefaults.enableAdministration { - /// 2.5 Administration with session passkey - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.securityConfig == nil { - _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) - } - } else { - if node.deviceConfig == nil { - /// Legacy Administration - _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + if node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 2.5 Administration with session passkey + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.securityConfig == nil { + _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } + } else { + if node.deviceConfig == nil { + /// Legacy Administration + _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + } } } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index e88816ab..481fb29d 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -346,7 +346,7 @@ struct Settings: View { if bleManager.connectedPeripheral != nil { Section("Configure") { if node?.canRemoteAdmin ?? false { - Picker("Configuring Node", selection: $selectedNode) { + Picker("Node", selection: $selectedNode) { if selectedNode == 0 { Text("Connect to a Node").tag(0) } @@ -365,6 +365,7 @@ struct Settings: View { } icon: { Image(systemName: "av.remote") } + .font(.caption2) .tag(Int(node.num)) } else if !UserDefaults.enableAdministration && node.metadata != nil { /// Nodes using the old admin system Label { @@ -390,8 +391,7 @@ struct Settings: View { } } } - .pickerStyle(.automatic) - .labelsHidden() + .pickerStyle(.navigationLink) .onChange(of: selectedNode) { newValue in if selectedNode > 0 { let node = nodes.first(where: { $0.num == newValue }) From d8cb04ddcc7dd1aee7ddc1f0cdf1c9cb25c1d1cc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 5 Sep 2024 19:38:28 -0700 Subject: [PATCH 194/333] Remove json logging --- Meshtastic/Helpers/BLEManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 11597647..87551c4f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -766,7 +766,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .waypointApp: waypointPacket(packet: decodedInfo.packet, context: context) case .nodeinfoApp: - MeshLogger.log("🕸️ MESH PACKET received for Node Info App for PKI LOCK debugging \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral.num, context: context) } From 8954b21faa15b212e66350621e51f12eb89f428b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 6 Sep 2024 09:21:40 -0700 Subject: [PATCH 195/333] stop unwrapping nuilable positon --- Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 7906f8b5..14df3986 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -130,7 +130,9 @@ struct NodeMapSwiftUI: View { if node.positions?.count ?? 0 > 1 { position = .automatic } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) + if let mr = mostRecent?.coordinate { + position = .camera(MapCamera(centerCoordinate: mr.coordinate, distance: 8000, heading: 0, pitch: 60)) + } } if self.scene == nil { Task { From 10751e374cffe21ae9f2c574ea08117ea2cbcd10 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 6 Sep 2024 09:23:54 -0700 Subject: [PATCH 196/333] Dont be dumb --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 43f27393..1e63593b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1686,7 +1686,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1721,7 +1721,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1753,7 +1753,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1786,7 +1786,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.3; + MARKETING_VERSION = 2.5.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 14df3986..340117ac 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -130,8 +130,8 @@ struct NodeMapSwiftUI: View { if node.positions?.count ?? 0 > 1 { position = .automatic } else { - if let mr = mostRecent?.coordinate { - position = .camera(MapCamera(centerCoordinate: mr.coordinate, distance: 8000, heading: 0, pitch: 60)) + if let mrCoord = mostRecent?.coordinate { + position = .camera(MapCamera(centerCoordinate: mrCoord, distance: 8000, heading: 0, pitch: 60)) } } if self.scene == nil { From 4f813c12d335db38de58dbdc6e600e2290ea7eab Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 6 Sep 2024 17:05:25 -0700 Subject: [PATCH 197/333] Clean up precision ranges for locations on the map report and public key --- Meshtastic/Views/Messages/UserList.swift | 46 +++++++++++++----- Meshtastic/Views/Settings/Channels.swift | 6 +-- .../Views/Settings/Channels/ChannelForm.swift | 2 +- .../Settings/Config/Module/MQTTConfig.swift | 48 ++++++------------- 4 files changed, 52 insertions(+), 50 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4e09859f..4db57180 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -35,7 +35,6 @@ struct UserList: View { var boolFilters: [Bool] {[ isFavorite, isOnline, - isPkiEncrypted, isEnvironment, distanceFilter, roleFilter @@ -45,11 +44,12 @@ struct UserList: View { 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: "hwModelId != nil"), + predicate: NSPredicate(format: "longName != ''"), animation: .default ) - private var users: FetchedResults + var users: FetchedResults @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? @@ -202,34 +202,55 @@ struct UserList: View { DirectMessagesHelp() } .onChange(of: searchText) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: viaLora) { _ in if !viaLora && !viaMqtt { viaMqtt = true } - searchUserList() + Task { + await searchUserList() + } } .onChange(of: viaMqtt) { _ in if !viaLora && !viaMqtt { viaLora = true } - searchUserList() + Task { + await searchUserList() + } } .onChange(of: [deviceRoles]) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: hopsAway) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: [boolFilters]) { _ in - searchUserList() + Task { + await searchUserList() + } } .onChange(of: maxDistance) { _ in - searchUserList() + Task { + await searchUserList() + } + } + .onChange(of: isPkiEncrypted) { _ in + Task { + await searchUserList() + } } .onAppear { - searchUserList() + Task { + await searchUserList() + } } .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { @@ -267,8 +288,7 @@ struct UserList: View { .scrollDismissesKeyboard(.immediately) } } - - private func searchUserList() { + private func searchUserList() async { /// Case Insensitive Search Text Predicates let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index f039534c..ca55b95d 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -93,7 +93,7 @@ struct Channels: View { preciseLocation = true positionsEnabled = true if channelKey == "AQ==" { - positionPrecision = 13 + positionPrecision = 14 preciseLocation = false } } else if !supportedVersion && channelRole == 2 { @@ -103,8 +103,8 @@ struct Channels: View { } else { if channelKey == "AQ==" { preciseLocation = false - if (positionPrecision > 0 && positionPrecision < 10) || positionPrecision > 16 { - positionPrecision = 13 + if (positionPrecision > 0 && positionPrecision < 11) || positionPrecision > 14 { + positionPrecision = 14 } } else if positionPrecision == 32 { preciseLocation = true diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index d21e0f42..51cb8dfc 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -158,7 +158,7 @@ struct ChannelForm: View { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 10...16, step: 1) { + Slider(value: $positionPrecision, in: 11...14, step: 1) { } minimumValueLabel: { Image(systemName: "minus") } maximumValueLabel: { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index d6cbbf2b..0caf3625 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -32,7 +32,6 @@ struct MQTTConfig: View { @State var nearbyTopics = [String]() @State var mapReportingEnabled = false @State var mapPublishIntervalSecs = 3600 - @State var preciseLocation: Bool = false @State var mapPositionPrecision: Double = 13.0 let locale = Locale.current @@ -105,35 +104,17 @@ struct MQTTConfig: View { } } .pickerStyle(DefaultPickerStyle()) - VStack(alignment: .leading) { - Toggle(isOn: $preciseLocation) { - Label("Precise Location", systemImage: "scope") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - .onChange(of: preciseLocation) { pl in - if pl == false { - mapPositionPrecision = 12 - } else { - mapPositionPrecision = 32 - } - } - } - - if !preciseLocation { - VStack(alignment: .leading) { - Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $mapPositionPrecision, in: 11...16, step: 1) { - } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { - Image(systemName: "plus") - } - Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") - .foregroundColor(.gray) - .font(.callout) + Label("Approximate Location", systemImage: "location.slash.circle.fill") + Slider(value: $mapPositionPrecision, in: 11...14, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") } + Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") + .foregroundColor(.gray) + .font(.callout) } } } @@ -429,11 +410,12 @@ struct MQTTConfig: View { self.mqttConnected = bleManager.mqttProxyConnected self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) - self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 12) - if mapPositionPrecision == 0.0 { - self.mapPositionPrecision = 12 + self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14) + if mapPositionPrecision < 11 || mapPositionPrecision > 14 { + self.mapPositionPrecision = 14 + self.hasChanges = true + } else { + self.hasChanges = false } - self.preciseLocation = mapPositionPrecision == 32 - self.hasChanges = false } } From 089022d3626639135c043878ab5b4ed1339148a1 Mon Sep 17 00:00:00 2001 From: Jacob Powers Date: Sun, 8 Sep 2024 04:47:14 +0000 Subject: [PATCH 198/333] add centerMapAt --- Meshtastic/Views/Nodes/MeshMap.swift | 34 ++++++++++++++++++++++++++-- Widgets/WidgetsLiveActivity.swift | 4 ++-- scripts/lint/lint-fix-changes.sh | 10 ++++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 595abb15..c4f987ab 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -33,6 +33,7 @@ struct MeshMap: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .excludingAll, showsTraffic: false) @State var position = MapCameraPosition.automatic + @State private var distance = 10000.0 @State private var editingSettings = false @State private var editingFilters = false @State var selectedPosition: PositionEntity? @@ -60,8 +61,19 @@ struct MeshMap: View { NavigationStack { ZStack { MapReader { reader in - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint) + Map( + position: $position, + bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), + scope: mapScope + ) { + MeshMapContent( + showUserLocation: $showUserLocation, + showTraffic: $showTraffic, + showPointsOfInterest: $showPointsOfInterest, + selectedMapLayer: $selectedMapLayer, + selectedPosition: $selectedPosition, + selectedWaypoint: $selectedWaypoint + ) } .mapScope(mapScope) .mapStyle(mapStyle) @@ -74,6 +86,9 @@ struct MeshMap: View { .mapControlVisibility(.automatic) } .controlSize(.regular) + .onMapCameraChange(frequency: MapCameraUpdateFrequency.continuous, { context in + distance = context.camera.distance + }) .onTapGesture(count: 1, perform: { position in newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init() }) @@ -92,6 +107,7 @@ struct MeshMap: View { Logger.services.error("Unable to convert local point to coordinate on map.") return } + centerMapAt(coordinate: coordinate) newWaypointCoord = coordinate editingWaypoint = WaypointEntity(context: context) @@ -210,4 +226,18 @@ struct MeshMap: View { UIApplication.shared.isIdleTimerDisabled = false }) } + + // moves the map to a new coordinate + private func centerMapAt(coordinate: CLLocationCoordinate2D) { + withAnimation(.easeInOut(duration: 0.2), { + position = .camera( + MapCamera( + centerCoordinate: coordinate, // Set new center + distance: distance, // Preserve current zoom distance + heading: 0, // align north + pitch: 0 // set view to top down + ) + ) + }) + } } diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index e6177129..154edf9d 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -120,7 +120,7 @@ struct WidgetsLiveActivity: Widget { } } -//struct WidgetsLiveActivity_Previews: PreviewProvider { +// struct WidgetsLiveActivity_Previews: PreviewProvider { // static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") // static let state = MeshActivityAttributes.ContentState( // timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) @@ -139,7 +139,7 @@ struct WidgetsLiveActivity: Widget { // .previewContext(state, viewKind: .content) // .previewDisplayName("Notification") // } -//} +// } struct LiveActivityView: View { @Environment(\.colorScheme) private var colorScheme diff --git a/scripts/lint/lint-fix-changes.sh b/scripts/lint/lint-fix-changes.sh index e4dae8d0..2de03ea2 100755 --- a/scripts/lint/lint-fix-changes.sh +++ b/scripts/lint/lint-fix-changes.sh @@ -23,22 +23,22 @@ if [[ -e "${SWIFT_LINT}" ]]; then ##### Fix files or exit if no files found for fixing ##### if [ "$count" -ne 0 ]; then echo "Found files to fix! Running swiftLint --fix..." - + # Run SwiftLint --fix on each file for ((i = 0; i < count; i++)); do file_var="SCRIPT_INPUT_FILE_$i" file_path=${!file_var} echo "Fixing $file_path" - $SWIFT_LINT --fix --path "$file_path" + $SWIFT_LINT --fix "$file_path" done - + # Add the fixed files back to staging for ((i = 0; i < count; i++)); do file_var="SCRIPT_INPUT_FILE_$i" file_path=${!file_var} git add "$file_path" done - + echo "swiftLint --fix completed and files re-staged." # Optionally lint the fixed files @@ -61,4 +61,4 @@ if [[ -e "${SWIFT_LINT}" ]]; then else echo "SwiftLint not installed. Please install from https://github.com/realm/SwiftLint" exit -1 -fi \ No newline at end of file +fi From adab69dab7adcdbb19f7e117e3669d724e9cf3e7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 8 Sep 2024 11:10:58 -0700 Subject: [PATCH 199/333] Default precision of 14 --- Meshtastic/Views/Settings/Channels/ChannelForm.swift | 6 +++--- Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 51cb8dfc..e4930b7a 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -149,7 +149,7 @@ struct ChannelForm: View { .listRowSeparator(.visible) .onChange(of: preciseLocation) { pl in if pl == false { - positionPrecision = 13 + positionPrecision = 14 } } } @@ -220,7 +220,7 @@ struct ChannelForm: View { } positionPrecision = 32 } else { - positionPrecision = 13 + positionPrecision = 14 } hasChanges = true } @@ -230,7 +230,7 @@ struct ChannelForm: View { .onChange(of: positionsEnabled) { pe in if pe { if positionPrecision == 0 { - positionPrecision = 13 + positionPrecision = 14 } } else { positionPrecision = 0 diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 0caf3625..1996c744 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -32,7 +32,7 @@ struct MQTTConfig: View { @State var nearbyTopics = [String]() @State var mapReportingEnabled = false @State var mapPublishIntervalSecs = 3600 - @State var mapPositionPrecision: Double = 13.0 + @State var mapPositionPrecision: Double = 14.0 let locale = Locale.current From 374924eb8229bcbd5cca64e471bd99586129acdc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 8 Sep 2024 12:56:52 -0700 Subject: [PATCH 200/333] Clean up the live activity --- Widgets/WidgetsLiveActivity.swift | 53 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index e6177129..e574b642 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -38,7 +38,7 @@ struct WidgetsLiveActivity: Widget { .fixedSize() Spacer() } - if context.state.nodesOnline >= 100 { + if context.state.totalNodes >= 100 { Text("100+ online") .font(.caption) .foregroundStyle(.secondary) @@ -81,7 +81,7 @@ struct WidgetsLiveActivity: Widget { .font(.caption) .foregroundStyle(.secondary) .fixedSize() - Text("Bad \(context.state.badReceivedPackets)") + Text("Bad: \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() @@ -97,7 +97,7 @@ struct WidgetsLiveActivity: Widget { } compactLeading: { Image("m-logo-black") .resizable() - .frame(width: 30.0) + .frame(width: 25) .padding(4) .background(.green.gradient, in: ContainerRelativeShape()) } compactTrailing: { @@ -120,26 +120,25 @@ struct WidgetsLiveActivity: Widget { } } -//struct WidgetsLiveActivity_Previews: PreviewProvider { -// static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") -// static let state = MeshActivityAttributes.ContentState( -// timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9) -// -// static var previews: some View { -// attributes -// .previewContext(state, viewKind: .dynamicIsland(.compact)) -// .previewDisplayName("Compact") -// attributes -// .previewContext(state, viewKind: .dynamicIsland(.minimal)) -// .previewDisplayName("Minimal") -// attributes -// .previewContext(state, viewKind: .dynamicIsland(.expanded)) -// .previewDisplayName("Expanded") -// attributes -// .previewContext(state, viewKind: .content) -// .previewDisplayName("Notification") -// } -//} +struct WidgetsLiveActivity_Previews: PreviewProvider { + static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") + static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) + + static var previews: some View { + attributes + .previewContext(state, viewKind: .dynamicIsland(.compact)) + .previewDisplayName("Compact") + attributes + .previewContext(state, viewKind: .dynamicIsland(.minimal)) + .previewDisplayName("Minimal") + attributes + .previewContext(state, viewKind: .dynamicIsland(.expanded)) + .previewDisplayName("Expanded") + attributes + .previewContext(state, viewKind: .content) + .previewDisplayName("Notification") + } +} struct LiveActivityView: View { @Environment(\.colorScheme) private var colorScheme @@ -203,13 +202,7 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Packets Sent: \(sentPackets)") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - Text("Packets Received: \(receivedPackets)") + Text("Packets: Sent \(sentPackets) Rec. \(receivedPackets)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) From f52a0918d04db07d9a39818b5aa63d6805fa64ed Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 8 Sep 2024 13:06:21 -0700 Subject: [PATCH 201/333] Add error rate to live activity --- Widgets/WidgetsLiveActivity.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index e574b642..7b9a9846 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -208,7 +208,8 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - Text("Bad Packets: \(badReceivedPackets)") + let errorRate = (Double(badReceivedPackets) / Double(receivedPackets)) * 100 + Text("Bad: \(badReceivedPackets) \(String(format: "Error Rate: %.2f", errorRate))%") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) From 050a02712de9e9e6953e0dc4014a1af573db0529 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 9 Sep 2024 11:38:07 -0700 Subject: [PATCH 202/333] Merge from main --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index b6237629..4da558d0 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit b623762940ebdb1887a3b31b86f4d9cdaa7e6ecf +Subproject commit 4da558d0f73c46ef91b74431facee73c09affbfc From 2daf3e353622480dd56faed0626a8bfe8b3b9377 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Sep 2024 15:05:13 -0700 Subject: [PATCH 203/333] Fix incoming keys --- Localizable.xcstrings | 28 +++++++++++++++------ Meshtastic/Helpers/MeshPackets.swift | 14 +++++++---- Meshtastic/Persistence/UpdateCoreData.swift | 21 ++++++++++------ Meshtastic/Views/Bluetooth/Connect.swift | 19 ++++++++------ Widgets/WidgetsLiveActivity.swift | 2 +- 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 05c2bbb8..b447989f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1429,8 +1429,15 @@ "Bad" : { }, - "Bad Packets: %d" : { - + "Bad Packets: %d %@%%" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bad Packets: %1$d %2$@%%" + } + } + } }, "Bandwidth" : { @@ -16079,11 +16086,15 @@ "Override automatic OLED screen detection." : { }, - "Packets Received: %d" : { - - }, - "Packets Sent: %d" : { - + "Packets: Sent: %d Received: %d" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Packets: Sent: %1$d Received: %2$d" + } + } + } }, "password" : { "localizations" : { @@ -22314,6 +22325,9 @@ } } } + }, + "Uptime: %@" : { + }, "Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead." : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3b6b2b65..d9801699 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -864,8 +864,10 @@ func textMessageAppPacket( newMessage.isEmoji = packet.decoded.emoji == 1 newMessage.channel = Int32(packet.channel) newMessage.portNum = Int32(packet.decoded.portnum.rawValue) - newMessage.publicKey = packet.publicKey - newMessage.pkiEncrypted = packet.pkiEncrypted + if newMessage.toUser?.pkiEncrypted ?? false { + newMessage.pkiEncrypted = true + newMessage.publicKey = packet.publicKey + } if packet.decoded.portnum == PortNum.detectionSensorApp { if !UserDefaults.enableDetectionNotifications { newMessage.read = true @@ -889,9 +891,11 @@ func textMessageAppPacket( newMessage.fromUser?.newPublicKey = newMessage.publicKey } } else { - /// We have no key, set it - newMessage.fromUser?.publicKey = packet.publicKey - newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted + /// We have no key, set it if it is not empty + if !packet.publicKey.isEmpty { + newMessage.fromUser?.pkiEncrypted = true + newMessage.fromUser?.publicKey = packet.publicKey + } } if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 624dbb20..c1ee8470 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -181,8 +181,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) - newUser.pkiEncrypted = packet.pkiEncrypted - newUser.publicKey = packet.publicKey + if !newUserMessage.publicKey.isEmpty { + newUser.pkiEncrypted = true + newUser.publicKey = newUserMessage.publicKey + } + Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) @@ -209,8 +212,10 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { if packet.from > Constants.minimumNodeNum { let newUser = createUser(num: Int64(packet.from), context: context) - newNode.user?.pkiEncrypted = packet.pkiEncrypted - newNode.user?.publicKey = packet.publicKey + if !packet.publicKey.isEmpty { + newNode.user?.pkiEncrypted = true + newNode.user?.publicKey = packet.publicKey + } newNode.user = newUser } } @@ -224,6 +229,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) myInfoEntity.rebootCount = 0 do { try context.save() + Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)") Logger.data.info("💾 [MyInfoEntity] Saved a new myInfo for node number: \(packet.from.toHex(), privacy: .public)") } catch { context.rollback() @@ -271,10 +277,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) 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) - - if !packet.publicKey.isEmpty { - fetchedNode[0].user!.pkiEncrypted = packet.pkiEncrypted - fetchedNode[0].user!.publicKey = packet.publicKey + if !nodeInfoMessage.user.publicKey.isEmpty { + fetchedNode[0].user!.pkiEncrypted = true + fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey } Task { Api().loadDeviceHardwareData { (hw) in diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 2a1eea84..3fdab7fd 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -92,6 +92,7 @@ struct Connect: View { } VStack { let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity + if localStats != nil { Divider() if localStats?.numTotalNodes ?? 0 >= 100 { @@ -111,25 +112,29 @@ struct Connect: View { .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) - Text("Packets Sent: \(localStats?.numPacketsTx ?? 0)") + Text("Packets: Sent: \(localStats?.numPacketsTx ?? 0) Received: \(localStats?.numPacketsRx ?? 0)") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) - Text("Packets Received: \(localStats?.numPacketsRx ?? 0)") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0)") + let errorRate = (Double(localStats?.numPacketsRxBad ?? -1) / Double(localStats?.numPacketsRx ?? -1)) * 100 + Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0) \(String(format: "Error Rate: %.2f", errorRate))%") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .fixedSize() + let now = Date.now + let later = now + TimeInterval(Double(localStats?.numPacketsRxBad ?? 0)) + let uptime = (now.. var body: some View { + let errorRate = (Double(badReceivedPackets) / Double(receivedPackets)) * 100 VStack(alignment: .leading, spacing: 0) { Text(nodeName) .font(nodeName.count > 14 ? .callout : .title3) @@ -208,7 +209,6 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - let errorRate = (Double(badReceivedPackets) / Double(receivedPackets)) * 100 Text("Bad: \(badReceivedPackets) \(String(format: "Error Rate: %.2f", errorRate))%") .font(.caption) .fontWeight(.medium) From 68475b0fef1e48ef49743c08e7c68e814a11282e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 12 Sep 2024 12:11:42 -0500 Subject: [PATCH 204/333] Update protos --- .../Sources/meshtastic/admin.pb.swift | 276 ++------- .../Sources/meshtastic/apponly.pb.swift | 6 +- .../Sources/meshtastic/atak.pb.swift | 64 +- .../meshtastic/cannedmessages.pb.swift | 6 +- .../Sources/meshtastic/channel.pb.swift | 37 +- .../Sources/meshtastic/clientonly.pb.swift | 6 +- .../Sources/meshtastic/config.pb.swift | 555 ++++++++--------- .../meshtastic/connection_status.pb.swift | 21 +- .../Sources/meshtastic/deviceonly.pb.swift | 38 +- .../Sources/meshtastic/localonly.pb.swift | 9 +- .../Sources/meshtastic/mesh.pb.swift | 566 ++++++------------ .../Sources/meshtastic/module_config.pb.swift | 263 +++----- .../Sources/meshtastic/mqtt.pb.swift | 9 +- .../Sources/meshtastic/paxcount.pb.swift | 6 +- .../Sources/meshtastic/portnums.pb.swift | 14 +- .../Sources/meshtastic/powermon.pb.swift | 115 ++-- .../meshtastic/remote_hardware.pb.swift | 35 +- .../Sources/meshtastic/rtttl.pb.swift | 6 +- .../Sources/meshtastic/storeforward.pb.swift | 93 +-- .../Sources/meshtastic/telemetry.pb.swift | 82 +-- .../Sources/meshtastic/xmodem.pb.swift | 39 +- protobufs | 2 +- 22 files changed, 731 insertions(+), 1517 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 0f1c3586..d9ecbe22 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -24,7 +24,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage { +public struct AdminMessage: @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. @@ -483,7 +483,7 @@ public struct AdminMessage { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -624,193 +624,11 @@ public struct AdminMessage { /// Tell the node to reset the nodedb. case nodedbReset(Int32) - #if !swift(>=4.1) - public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.getChannelRequest, .getChannelRequest): return { - guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getChannelResponse, .getChannelResponse): return { - guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerRequest, .getOwnerRequest): return { - guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerResponse, .getOwnerResponse): return { - guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigRequest, .getConfigRequest): return { - guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigResponse, .getConfigResponse): return { - guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigRequest, .getModuleConfigRequest): return { - guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigResponse, .getModuleConfigResponse): return { - guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { - guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { - guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { - guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { - guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneRequest, .getRingtoneRequest): return { - guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneResponse, .getRingtoneResponse): return { - guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { - guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { - guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setHamMode, .setHamMode): return { - guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { - guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { - guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.enterDfuModeRequest, .enterDfuModeRequest): return { - guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deleteFileRequest, .deleteFileRequest): return { - guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setScale, .setScale): return { - guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setOwner, .setOwner): return { - guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setChannel, .setChannel): return { - guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setConfig, .setConfig): return { - guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setModuleConfig, .setModuleConfig): return { - guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { - guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setRingtoneMessage, .setRingtoneMessage): return { - guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeByNodenum, .removeByNodenum): return { - guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFavoriteNode, .setFavoriteNode): return { - guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFavoriteNode, .removeFavoriteNode): return { - guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFixedPosition, .setFixedPosition): return { - guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFixedPosition, .removeFixedPosition): return { - guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setTimeOnly, .setTimeOnly): return { - guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.beginEditSettings, .beginEditSettings): return { - guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.commitEditSettings, .commitEditSettings): return { - guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetDevice, .factoryResetDevice): return { - guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootOtaSeconds, .rebootOtaSeconds): return { - guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.exitSimulator, .exitSimulator): return { - guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootSeconds, .rebootSeconds): return { - guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.shutdownSeconds, .shutdownSeconds): return { - guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetConfig, .factoryResetConfig): return { - guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodedbReset, .nodedbReset): return { - guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum { + public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -844,6 +662,7 @@ public struct AdminMessage { /// /// TODO: REPLACE case securityConfig // = 7 + case sessionkeyConfig // = 8 case UNRECOGNIZED(Int) public init() { @@ -860,6 +679,7 @@ public struct AdminMessage { case 5: self = .loraConfig case 6: self = .bluetoothConfig case 7: self = .securityConfig + case 8: self = .sessionkeyConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -874,15 +694,29 @@ public struct AdminMessage { case .loraConfig: return 5 case .bluetoothConfig: return 6 case .securityConfig: return 7 + case .sessionkeyConfig: return 8 case .UNRECOGNIZED(let i): return i } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + .securityConfig, + .sessionkeyConfig, + ] + } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum { + public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -980,51 +814,31 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] + } public init() {} } -#if swift(>=4.2) - -extension AdminMessage.ConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - .securityConfig, - ] -} - -extension AdminMessage.ModuleConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] -} - -#endif // swift(>=4.2) - /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters { +public struct HamParameters: 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. @@ -1054,7 +868,7 @@ public struct HamParameters { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse { +public struct NodeRemoteHardwarePinsResponse: 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. @@ -1068,15 +882,6 @@ public struct NodeRemoteHardwarePinsResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension AdminMessage: @unchecked Sendable {} -extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension AdminMessage.ConfigType: @unchecked Sendable {} -extension AdminMessage.ModuleConfigType: @unchecked Sendable {} -extension HamParameters: @unchecked Sendable {} -extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1755,6 +1560,7 @@ extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "LORA_CONFIG"), 6: .same(proto: "BLUETOOTH_CONFIG"), 7: .same(proto: "SECURITY_CONFIG"), + 8: .same(proto: "SESSIONKEY_CONFIG"), ] } @@ -1807,7 +1613,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency != 0 { + if self.frequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 0457077c..18e66d8e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet { +public struct ChannelSet: 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. @@ -53,10 +53,6 @@ public struct ChannelSet { fileprivate var _loraConfig: Config.LoRaConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSet: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 4406deb3..1dd12469 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum { +public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -130,11 +130,6 @@ public enum Team: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -153,13 +148,12 @@ extension Team: CaseIterable { .darkGreen, .brown, ] -} -#endif // swift(>=4.2) +} /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum { +public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -233,11 +227,6 @@ public enum MemberRole: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -250,13 +239,12 @@ extension MemberRole: CaseIterable { .rto, .k9, ] -} -#endif // swift(>=4.2) +} /// /// Packets for the official ATAK Plugin -public struct TAKPacket { +public struct TAKPacket: 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. @@ -326,7 +314,7 @@ public struct TAKPacket { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TAK position report case pli(PLI) @@ -334,24 +322,6 @@ public struct TAKPacket { /// ATAK GeoChat message case chat(GeoChat) - #if !swift(>=4.1) - public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.pli, .pli): return { - guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.chat, .chat): return { - guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -363,7 +333,7 @@ public struct TAKPacket { /// /// ATAK GeoChat message -public struct GeoChat { +public struct GeoChat: 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. @@ -405,7 +375,7 @@ public struct GeoChat { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group { +public struct Group: 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. @@ -427,7 +397,7 @@ public struct Group { /// /// ATAK EUD Status /// -public struct Status { +public struct Status: 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. @@ -444,7 +414,7 @@ public struct Status { /// /// ATAK Contact /// -public struct Contact { +public struct Contact: 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. @@ -464,7 +434,7 @@ public struct Contact { /// /// Position Location Information from ATAK -public struct PLI { +public struct PLI: 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. @@ -496,18 +466,6 @@ public struct PLI { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Team: @unchecked Sendable {} -extension MemberRole: @unchecked Sendable {} -extension TAKPacket: @unchecked Sendable {} -extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension GeoChat: @unchecked Sendable {} -extension Group: @unchecked Sendable {} -extension Status: @unchecked Sendable {} -extension Contact: @unchecked Sendable {} -extension PLI: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..a43393e1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig { +public struct CannedMessageModuleConfig: 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. @@ -36,10 +36,6 @@ public struct CannedMessageModuleConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension CannedMessageModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index 5b9c7e49..a8c96595 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -36,13 +36,15 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings { +public struct ChannelSettings: @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. /// /// Deprecated in favor of LoraConfig.channel_num + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -111,7 +113,7 @@ public struct ChannelSettings { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings { +public struct ModuleSettings: 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. @@ -132,7 +134,7 @@ public struct ModuleSettings { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel { +public struct Channel: 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. @@ -170,7 +172,7 @@ public struct Channel { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -209,6 +211,13 @@ public struct Channel { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] + } public init() {} @@ -216,26 +225,6 @@ public struct Channel { fileprivate var _settings: ChannelSettings? = nil } -#if swift(>=4.2) - -extension Channel.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSettings: @unchecked Sendable {} -extension ModuleSettings: @unchecked Sendable {} -extension Channel: @unchecked Sendable {} -extension Channel.Role: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index c3d93bf7..89370cc5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile { +public struct DeviceProfile: 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. @@ -94,10 +94,6 @@ public struct DeviceProfile { fileprivate var _moduleConfig: LocalModuleConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceProfile: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 4b953470..2bb116d3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config { +public struct Config: 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. @@ -93,11 +93,19 @@ public struct Config { set {payloadVariant = .security(newValue)} } + public var sessionkey: Config.SessionkeyConfig { + get { + if case .sessionkey(let v)? = payloadVariant {return v} + return Config.SessionkeyConfig() + } + set {payloadVariant = .sessionkey(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -106,54 +114,13 @@ public struct Config { case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) case security(Config.SecurityConfig) + case sessionkey(Config.SessionkeyConfig) - #if !swift(>=4.1) - public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.device, .device): return { - guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.position, .position): return { - guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.power, .power): return { - guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.network, .network): return { - guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.display, .display): return { - guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lora, .lora): return { - guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.bluetooth, .bluetooth): return { - guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.security, .security): return { - guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// Configuration - public struct DeviceConfig { + public struct DeviceConfig: 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. @@ -165,13 +132,9 @@ public struct Config { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI /// Moved to SecurityConfig - public var serialEnabled: Bool = false - /// - /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Set this to true to leave the debug log outputting even when API is active. - /// Moved to SecurityConfig - public var debugLogEnabled: Bool = false + /// NOTE: This field was marked as deprecated in the .proto file. + public var serialEnabled: Bool = false /// /// For boards without a hard wired button, this is the pin number that will be used @@ -200,6 +163,8 @@ public struct Config { /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var isManaged: Bool = false /// @@ -218,7 +183,7 @@ public struct Config { /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -236,6 +201,8 @@ public struct Config { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 + + /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -326,11 +293,26 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + ] + } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum { + public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -378,6 +360,14 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + ] + } public init() {} @@ -385,7 +375,7 @@ public struct Config { /// /// Position Config - public struct PositionConfig { + public struct PositionConfig: 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. @@ -407,6 +397,8 @@ public struct Config { /// /// Is GPS enabled for this node? + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -417,6 +409,8 @@ public struct Config { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -457,7 +451,7 @@ public struct Config { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum { + public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -547,9 +541,24 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] + } - public enum GpsMode: SwiftProtobuf.Enum { + public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -587,6 +596,13 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] + } public init() {} @@ -595,7 +611,7 @@ public struct Config { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig { + public struct PowerConfig: 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. @@ -655,7 +671,7 @@ public struct Config { /// /// Network Config - public struct NetworkConfig { + public struct NetworkConfig: 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. @@ -702,7 +718,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum { + public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -734,9 +750,15 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] + } - public struct IpV4Config { + public struct IpV4Config: 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. @@ -769,7 +791,7 @@ public struct Config { /// /// Display Config - public struct DisplayConfig { + public struct DisplayConfig: 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. @@ -825,7 +847,7 @@ public struct Config { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -888,11 +910,21 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] + } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum { + public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -924,11 +956,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] + } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum { + public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -972,9 +1010,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + ] + } - public enum DisplayMode: SwiftProtobuf.Enum { + public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1018,9 +1064,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] + } - public enum CompassOrientation: SwiftProtobuf.Enum { + public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1088,6 +1142,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] + } public init() {} @@ -1095,7 +1161,7 @@ public struct Config { /// /// Lora Config - public struct LoRaConfig { + public struct LoRaConfig: @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. @@ -1250,9 +1316,16 @@ public struct Config { set {_uniqueStorage()._ignoreMqtt = newValue} } + /// + /// Sets the ok_to_mqtt bit on outgoing packets + public var configOkToMqtt: Bool { + get {return _storage._configOkToMqtt} + set {_uniqueStorage()._configOkToMqtt = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum { + public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1386,12 +1459,35 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + ] + } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum { + public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1405,6 +1501,8 @@ public struct Config { /// /// Very Long Range - Slow /// Deprecated in 2.5: Works only with txco and is unusably slow + /// + /// NOTE: This enum value was marked as deprecated in the .proto file case veryLongSlow // = 2 /// @@ -1468,6 +1566,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + .shortTurbo, + ] + } public init() {} @@ -1475,7 +1586,7 @@ public struct Config { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig { + public struct BluetoothConfig: 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. @@ -1492,14 +1603,9 @@ public struct Config { /// Specified PIN for PairingMode.FixedPin public var fixedPin: UInt32 = 0 - /// - /// Enables device (serial style logs) over Bluetooth - /// Moved to SecurityConfig - public var deviceLoggingEnabled: Bool = false - public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum { + public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1537,12 +1643,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] + } public init() {} } - public struct SecurityConfig { + public struct SecurityConfig: @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. @@ -1559,7 +1672,7 @@ public struct Config { /// /// The public key authorized to send admin messages to this node. - public var adminKey: Data = Data() + public var adminKey: [Data] = [] /// /// If true, device is considered to be "managed" by a mesh administrator via admin messages @@ -1572,13 +1685,9 @@ public struct Config { /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Output live debug logging over serial. + /// Output live debug logging over serial or bluetooth is set to true. public var debugLogApiEnabled: Bool = false - /// - /// Enables device (serial style logs) over Bluetooth - public var bluetoothLoggingEnabled: Bool = false - /// /// Allow incoming device control over the insecure legacy admin channel. public var adminChannelEnabled: Bool = false @@ -1588,204 +1697,21 @@ public struct Config { public init() {} } + /// + /// Blank config request, strictly for getting the session key + public struct SessionkeyConfig: 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 unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } -#if swift(>=4.2) - -extension Config.DeviceConfig.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - ] -} - -extension Config.DeviceConfig.RebroadcastMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - ] -} - -extension Config.PositionConfig.PositionFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] -} - -extension Config.PositionConfig.GpsMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] -} - -extension Config.NetworkConfig.AddressMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] -} - -extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] -} - -extension Config.DisplayConfig.DisplayUnits: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] -} - -extension Config.DisplayConfig.OledType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - ] -} - -extension Config.DisplayConfig.DisplayMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] -} - -extension Config.DisplayConfig.CompassOrientation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] -} - -extension Config.LoRaConfig.RegionCode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - ] -} - -extension Config.LoRaConfig.ModemPreset: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - .shortTurbo, - ] -} - -extension Config.BluetoothConfig.PairingMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension Config: @unchecked Sendable {} -extension Config.OneOf_PayloadVariant: @unchecked Sendable {} -extension Config.DeviceConfig: @unchecked Sendable {} -extension Config.DeviceConfig.Role: @unchecked Sendable {} -extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} -extension Config.PositionConfig: @unchecked Sendable {} -extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} -extension Config.PositionConfig.GpsMode: @unchecked Sendable {} -extension Config.PowerConfig: @unchecked Sendable {} -extension Config.NetworkConfig: @unchecked Sendable {} -extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} -extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} -extension Config.DisplayConfig: @unchecked Sendable {} -extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} -extension Config.DisplayConfig.OledType: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} -extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} -extension Config.LoRaConfig: @unchecked Sendable {} -extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} -extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} -extension Config.BluetoothConfig: @unchecked Sendable {} -extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} -extension Config.SecurityConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1801,6 +1727,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), 8: .same(proto: "security"), + 9: .same(proto: "sessionkey"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1913,6 +1840,19 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas self.payloadVariant = .security(v) } }() + case 9: try { + var v: Config.SessionkeyConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .sessionkey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .sessionkey(v) + } + }() default: break } } @@ -1956,6 +1896,10 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas guard case .security(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 8) }() + case .sessionkey?: try { + guard case .sessionkey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1973,7 +1917,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "role"), 2: .standard(proto: "serial_enabled"), - 3: .standard(proto: "debug_log_enabled"), 4: .standard(proto: "button_gpio"), 5: .standard(proto: "buzzer_gpio"), 6: .standard(proto: "rebroadcast_mode"), @@ -1993,7 +1936,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl switch fieldNumber { case 1: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 2: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.debugLogEnabled) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self.buttonGpio) }() case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }() @@ -2015,9 +1957,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.serialEnabled != false { try visitor.visitSingularBoolField(value: self.serialEnabled, fieldNumber: 2) } - if self.debugLogEnabled != false { - try visitor.visitSingularBoolField(value: self.debugLogEnabled, fieldNumber: 3) - } if self.buttonGpio != 0 { try visitor.visitSingularUInt32Field(value: self.buttonGpio, fieldNumber: 4) } @@ -2051,7 +1990,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static func ==(lhs: Config.DeviceConfig, rhs: Config.DeviceConfig) -> Bool { if lhs.role != rhs.role {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} - if lhs.debugLogEnabled != rhs.debugLogEnabled {return false} if lhs.buttonGpio != rhs.buttonGpio {return false} if lhs.buzzerGpio != rhs.buzzerGpio {return false} if lhs.rebroadcastMode != rhs.rebroadcastMode {return false} @@ -2260,7 +2198,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride != 0 { + if self.adcMultiplierOverride.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2595,6 +2533,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 15: .standard(proto: "pa_fan_disabled"), 103: .standard(proto: "ignore_incoming"), 104: .standard(proto: "ignore_mqtt"), + 105: .standard(proto: "config_ok_to_mqtt"), ] fileprivate class _StorageClass { @@ -2615,6 +2554,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _paFanDisabled: Bool = false var _ignoreIncoming: [UInt32] = [] var _ignoreMqtt: Bool = false + var _configOkToMqtt: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -2646,6 +2586,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem _paFanDisabled = source._paFanDisabled _ignoreIncoming = source._ignoreIncoming _ignoreMqtt = source._ignoreMqtt + _configOkToMqtt = source._configOkToMqtt } } @@ -2681,6 +2622,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem case 15: try { try decoder.decodeSingularBoolField(value: &_storage._paFanDisabled) }() case 103: try { try decoder.decodeRepeatedUInt32Field(value: &_storage._ignoreIncoming) }() case 104: try { try decoder.decodeSingularBoolField(value: &_storage._ignoreMqtt) }() + case 105: try { try decoder.decodeSingularBoolField(value: &_storage._configOkToMqtt) }() default: break } } @@ -2704,7 +2646,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset != 0 { + if _storage._frequencyOffset.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2728,7 +2670,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency != 0 { + if _storage._overrideFrequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { @@ -2740,6 +2682,9 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._ignoreMqtt != false { try visitor.visitSingularBoolField(value: _storage._ignoreMqtt, fieldNumber: 104) } + if _storage._configOkToMqtt != false { + try visitor.visitSingularBoolField(value: _storage._configOkToMqtt, fieldNumber: 105) + } } try unknownFields.traverse(visitor: &visitor) } @@ -2766,6 +2711,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._paFanDisabled != rhs_storage._paFanDisabled {return false} if _storage._ignoreIncoming != rhs_storage._ignoreIncoming {return false} if _storage._ignoreMqtt != rhs_storage._ignoreMqtt {return false} + if _storage._configOkToMqtt != rhs_storage._configOkToMqtt {return false} return true } if !storagesAreEqual {return false} @@ -2819,7 +2765,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI 1: .same(proto: "enabled"), 2: .same(proto: "mode"), 3: .standard(proto: "fixed_pin"), - 4: .standard(proto: "device_logging_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2831,7 +2776,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }() case 2: try { try decoder.decodeSingularEnumField(value: &self.mode) }() case 3: try { try decoder.decodeSingularUInt32Field(value: &self.fixedPin) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.deviceLoggingEnabled) }() default: break } } @@ -2847,9 +2791,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if self.fixedPin != 0 { try visitor.visitSingularUInt32Field(value: self.fixedPin, fieldNumber: 3) } - if self.deviceLoggingEnabled != false { - try visitor.visitSingularBoolField(value: self.deviceLoggingEnabled, fieldNumber: 4) - } try unknownFields.traverse(visitor: &visitor) } @@ -2857,7 +2798,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if lhs.enabled != rhs.enabled {return false} if lhs.mode != rhs.mode {return false} if lhs.fixedPin != rhs.fixedPin {return false} - if lhs.deviceLoggingEnabled != rhs.deviceLoggingEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2880,7 +2820,6 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 4: .standard(proto: "is_managed"), 5: .standard(proto: "serial_enabled"), 6: .standard(proto: "debug_log_api_enabled"), - 7: .standard(proto: "bluetooth_logging_enabled"), 8: .standard(proto: "admin_channel_enabled"), ] @@ -2892,11 +2831,10 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm switch fieldNumber { case 1: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() case 2: try { try decoder.decodeSingularBytesField(value: &self.privateKey) }() - case 3: try { try decoder.decodeSingularBytesField(value: &self.adminKey) }() + case 3: try { try decoder.decodeRepeatedBytesField(value: &self.adminKey) }() case 4: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() case 5: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogApiEnabled) }() - case 7: try { try decoder.decodeSingularBoolField(value: &self.bluetoothLoggingEnabled) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.adminChannelEnabled) }() default: break } @@ -2911,7 +2849,7 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm try visitor.visitSingularBytesField(value: self.privateKey, fieldNumber: 2) } if !self.adminKey.isEmpty { - try visitor.visitSingularBytesField(value: self.adminKey, fieldNumber: 3) + try visitor.visitRepeatedBytesField(value: self.adminKey, fieldNumber: 3) } if self.isManaged != false { try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 4) @@ -2922,9 +2860,6 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.debugLogApiEnabled != false { try visitor.visitSingularBoolField(value: self.debugLogApiEnabled, fieldNumber: 6) } - if self.bluetoothLoggingEnabled != false { - try visitor.visitSingularBoolField(value: self.bluetoothLoggingEnabled, fieldNumber: 7) - } if self.adminChannelEnabled != false { try visitor.visitSingularBoolField(value: self.adminChannelEnabled, fieldNumber: 8) } @@ -2938,9 +2873,27 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.isManaged != rhs.isManaged {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} if lhs.debugLogApiEnabled != rhs.debugLogApiEnabled {return false} - if lhs.bluetoothLoggingEnabled != rhs.bluetoothLoggingEnabled {return false} if lhs.adminChannelEnabled != rhs.adminChannelEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } + +extension Config.SessionkeyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Config.protoMessageName + ".SessionkeyConfig" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Config.SessionkeyConfig, rhs: Config.SessionkeyConfig) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a2ec180e..a4569714 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus { +public struct DeviceConnectionStatus: 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. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus { /// /// WiFi connection status -public struct WifiConnectionStatus { +public struct WifiConnectionStatus: 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. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus { /// /// Ethernet connection status -public struct EthernetConnectionStatus { +public struct EthernetConnectionStatus: 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. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus { +public struct NetworkConnectionStatus: 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. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus { +public struct BluetoothConnectionStatus: 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. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus { /// /// Serial connection status -public struct SerialConnectionStatus { +public struct SerialConnectionStatus: 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. @@ -209,15 +209,6 @@ public struct SerialConnectionStatus { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceConnectionStatus: @unchecked Sendable {} -extension WifiConnectionStatus: @unchecked Sendable {} -extension EthernetConnectionStatus: @unchecked Sendable {} -extension NetworkConnectionStatus: @unchecked Sendable {} -extension BluetoothConnectionStatus: @unchecked Sendable {} -extension SerialConnectionStatus: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 43506399..076e639e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum { +public enum ScreenFonts: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -60,24 +60,18 @@ public enum ScreenFonts: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, ] -} -#endif // swift(>=4.2) +} /// /// Position with static location information only for NodeDBLite -public struct PositionLite { +public struct PositionLite: 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. @@ -112,13 +106,15 @@ public struct PositionLite { public init() {} } -public struct UserLite { +public struct UserLite: @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. /// /// This is the addr of the radio. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -157,7 +153,7 @@ public struct UserLite { public init() {} } -public struct NodeInfoLite { +public struct NodeInfoLite: @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. @@ -260,7 +256,7 @@ public struct NodeInfoLite { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState { +public struct DeviceState: @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. @@ -320,6 +316,8 @@ public struct DeviceState { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -368,7 +366,7 @@ public struct DeviceState { /// /// The on-disk saved channels -public struct ChannelFile { +public struct ChannelFile: 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. @@ -391,7 +389,7 @@ public struct ChannelFile { /// /// This can be used for customizing the firmware distribution. If populated, /// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore { +public struct OEMStore: @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. @@ -450,16 +448,6 @@ public struct OEMStore { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ScreenFonts: @unchecked Sendable {} -extension PositionLite: @unchecked Sendable {} -extension UserLite: @unchecked Sendable {} -extension NodeInfoLite: @unchecked Sendable {} -extension DeviceState: @unchecked Sendable {} -extension ChannelFile: @unchecked Sendable {} -extension OEMStore: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -695,7 +683,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 0af27466..f2ef681d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig { +public struct LocalConfig: @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. @@ -129,7 +129,7 @@ public struct LocalConfig { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig { +public struct LocalModuleConfig: @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. @@ -293,11 +293,6 @@ public struct LocalModuleConfig { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=5.5) && canImport(_Concurrency) -extension LocalConfig: @unchecked Sendable {} -extension LocalModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index ba12908d..a4dfa4cb 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -25,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum { +public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -346,6 +346,19 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. case me25Ls014Y10Td // = 75 + /// + /// RP2040_FEATHER_RFM95 + /// Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED + /// https://www.adafruit.com/product/5714 + /// https://www.adafruit.com/product/326 + /// https://www.adafruit.com/product/938 + /// ^^^ short A0 to switch to I2C address 0x3C + case rp2040FeatherRfm95 // = 76 + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + case m5StackCorebasic // = 77 + case m5StackCore2 // = 78 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -434,6 +447,9 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 73: self = .wioE5 case 74: self = .radiomaster900Bandit case 75: self = .me25Ls014Y10Td + case 76: self = .rp2040FeatherRfm95 + case 77: self = .m5StackCorebasic + case 78: self = .m5StackCore2 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -516,16 +532,14 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .wioE5: return 73 case .radiomaster900Bandit: return 74 case .me25Ls014Y10Td: return 75 + case .rp2040FeatherRfm95: return 76 + case .m5StackCorebasic: return 77 + case .m5StackCore2: return 78 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -603,15 +617,17 @@ extension HardwareModel: CaseIterable { .wioE5, .radiomaster900Bandit, .me25Ls014Y10Td, + .rp2040FeatherRfm95, + .m5StackCorebasic, + .m5StackCore2, .privateHw, ] -} -#endif // swift(>=4.2) +} /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum { +public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -646,26 +662,20 @@ public enum Constants: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] -} -#endif // swift(>=4.2) +} /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum { +public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -774,11 +784,6 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -796,13 +801,12 @@ extension CriticalErrorCode: CaseIterable { .flashCorruptionRecoverable, .flashCorruptionUnrecoverable, ] -} -#endif // swift(>=4.2) +} /// /// a gps position -public struct Position { +public struct Position: @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. @@ -1019,7 +1023,7 @@ public struct Position { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum { + public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1063,12 +1067,20 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] + } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum { + public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1118,6 +1130,15 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] + } public init() {} @@ -1125,31 +1146,6 @@ public struct Position { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension Position.LocSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] -} - -extension Position.AltSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] -} - -#endif // swift(>=4.2) - /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1171,7 +1167,7 @@ extension Position.AltSource: CaseIterable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User { +public struct User: @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. @@ -1196,6 +1192,8 @@ public struct User { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1227,7 +1225,7 @@ public struct User { /// /// A message used in a traceroute -public struct RouteDiscovery { +public struct RouteDiscovery: 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. @@ -1255,7 +1253,7 @@ public struct RouteDiscovery { /// /// A Routing control Data packet handled by the routing module -public struct Routing { +public struct Routing: 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. @@ -1295,7 +1293,7 @@ public struct Routing { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1307,34 +1305,12 @@ public struct Routing { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) - #if !swift(>=4.1) - public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.routeRequest, .routeRequest): return { - guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.routeReply, .routeReply): return { - guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorReason, .errorReason): return { - guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum { + public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1440,40 +1416,34 @@ public struct Routing { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + .pkiFailed, + .pkiUnknownPubkey, + ] + } public init() {} } -#if swift(>=4.2) - -extension Routing.Error: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - .pkiFailed, - .pkiUnknownPubkey, - ] -} - -#endif // swift(>=4.2) - /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage { +public struct DataMessage: @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. @@ -1520,14 +1490,27 @@ public struct DataMessage { /// a message a heart or poop emoji. public var emoji: UInt32 = 0 + /// + /// Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. + public var bitfield: UInt32 { + get {return _bitfield ?? 0} + set {_bitfield = newValue} + } + /// Returns true if `bitfield` has been explicitly set. + public var hasBitfield: Bool {return self._bitfield != nil} + /// Clears the value of `bitfield`. Subsequent reads from it will return its default value. + public mutating func clearBitfield() {self._bitfield = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _bitfield: UInt32? = nil } /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint { +public struct Waypoint: 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. @@ -1589,7 +1572,7 @@ public struct Waypoint { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage { +public struct MqttClientProxyMessage: @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. @@ -1630,7 +1613,7 @@ public struct MqttClientProxyMessage { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// Bytes case data(Data) @@ -1638,24 +1621,6 @@ public struct MqttClientProxyMessage { /// Text case text(String) - #if !swift(>=4.1) - public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.data, .data): return { - guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -1665,7 +1630,7 @@ public struct MqttClientProxyMessage { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket { +public struct MeshPacket: @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. @@ -1799,6 +1764,8 @@ public struct MeshPacket { /// /// Describe if this message is delayed + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -1835,7 +1802,7 @@ public struct MeshPacket { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1843,24 +1810,6 @@ public struct MeshPacket { /// TODO: REPLACE case encrypted(Data) - #if !swift(>=4.1) - public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.decoded, .decoded): return { - guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.encrypted, .encrypted): return { - guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// @@ -1882,7 +1831,7 @@ public struct MeshPacket { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum { + public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1907,6 +1856,15 @@ public struct MeshPacket { /// assume it is important and use a slightly higher priority case reliable // = 70 + /// + /// If priority is unset but the packet is a response to a request, we want it to get there relatively quickly. + /// Furthermore, responses stop relaying packets directed to a node early. + case response // = 80 + + /// + /// Higher priority for specific message types (portnums) to distinguish between other reliable packets. + case high // = 100 + /// /// Ack/naks are sent with very high priority to ensure that retransmission /// stops as soon as possible @@ -1928,6 +1886,8 @@ public struct MeshPacket { case 10: self = .background case 64: self = .default case 70: self = .reliable + case 80: self = .response + case 100: self = .high case 120: self = .ack case 127: self = .max default: self = .UNRECOGNIZED(rawValue) @@ -1941,17 +1901,32 @@ public struct MeshPacket { case .background: return 10 case .default: return 64 case .reliable: return 70 + case .response: return 80 + case .high: return 100 case .ack: return 120 case .max: return 127 case .UNRECOGNIZED(let i): return i } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .response, + .high, + .ack, + .max, + ] + } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum { + public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1989,6 +1964,13 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] + } public init() {} @@ -1996,32 +1978,6 @@ public struct MeshPacket { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension MeshPacket.Priority: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .ack, - .max, - ] -} - -extension MeshPacket.Delayed: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] -} - -#endif // swift(>=4.2) - /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -2039,7 +1995,7 @@ extension MeshPacket.Delayed: CaseIterable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo { +public struct NodeInfo: @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. @@ -2140,7 +2096,7 @@ public struct NodeInfo { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo { +public struct MyNodeInfo: 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. @@ -2171,7 +2127,7 @@ public struct MyNodeInfo { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord { +public struct LogRecord: 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. @@ -2196,7 +2152,7 @@ public struct LogRecord { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum { + public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2258,29 +2214,23 @@ public struct LogRecord { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] + } public init() {} } -#if swift(>=4.2) - -extension LogRecord.Level: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] -} - -#endif // swift(>=4.2) - -public struct QueueStatus { +public struct QueueStatus: 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. @@ -2307,7 +2257,7 @@ public struct QueueStatus { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio { +public struct FromRadio: 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. @@ -2483,7 +2433,7 @@ public struct FromRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2538,76 +2488,6 @@ public struct FromRadio { /// Notification message to the client case clientNotification(ClientNotification) - #if !swift(>=4.1) - public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.myInfo, .myInfo): return { - guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodeInfo, .nodeInfo): return { - guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.config, .config): return { - guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.logRecord, .logRecord): return { - guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.configCompleteID, .configCompleteID): return { - guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebooted, .rebooted): return { - guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.moduleConfig, .moduleConfig): return { - guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.channel, .channel): return { - guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.queueStatus, .queueStatus): return { - guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.metadata, .metadata): return { - guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileInfo, .fileInfo): return { - guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.clientNotification, .clientNotification): return { - guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2618,7 +2498,7 @@ public struct FromRadio { /// To be used for important messages that should to be displayed to the user /// in the form of push notifications or validation messages when saving /// invalid configuration. -public struct ClientNotification { +public struct ClientNotification: 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. @@ -2655,7 +2535,7 @@ public struct ClientNotification { /// /// Individual File info for the device -public struct FileInfo { +public struct FileInfo: 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. @@ -2676,7 +2556,7 @@ public struct FileInfo { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio { +public struct ToRadio: 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. @@ -2756,7 +2636,7 @@ public struct ToRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -2783,40 +2663,6 @@ public struct ToRadio { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) - #if !swift(>=4.1) - public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.wantConfigID, .wantConfigID): return { - guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.disconnect, .disconnect): return { - guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2824,7 +2670,7 @@ public struct ToRadio { /// /// Compressed message payload -public struct Compressed { +public struct Compressed: @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. @@ -2844,7 +2690,7 @@ public struct Compressed { /// /// Full info on edges for a single node -public struct NeighborInfo { +public struct NeighborInfo: 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. @@ -2872,7 +2718,7 @@ public struct NeighborInfo { /// /// A single edge in the mesh -public struct Neighbor { +public struct Neighbor: 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. @@ -2902,7 +2748,7 @@ public struct Neighbor { /// /// Device metadata response -public struct DeviceMetadata { +public struct DeviceMetadata: 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. @@ -2955,7 +2801,7 @@ public struct DeviceMetadata { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat { +public struct Heartbeat: 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. @@ -2967,7 +2813,7 @@ public struct Heartbeat { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin { +public struct NodeRemoteHardwarePin: 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. @@ -2994,7 +2840,7 @@ public struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload { +public struct ChunkedPayload: @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. @@ -3022,7 +2868,7 @@ public struct ChunkedPayload { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks { +public struct resend_chunks: 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. @@ -3036,7 +2882,7 @@ public struct resend_chunks { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse { +public struct ChunkedPayloadResponse: 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. @@ -3079,7 +2925,7 @@ public struct ChunkedPayloadResponse { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -3090,76 +2936,11 @@ public struct ChunkedPayloadResponse { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) - #if !swift(>=4.1) - public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.requestTransfer, .requestTransfer): return { - guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.acceptTransfer, .acceptTransfer): return { - guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.resendChunks, .resendChunks): return { - guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareModel: @unchecked Sendable {} -extension Constants: @unchecked Sendable {} -extension CriticalErrorCode: @unchecked Sendable {} -extension Position: @unchecked Sendable {} -extension Position.LocSource: @unchecked Sendable {} -extension Position.AltSource: @unchecked Sendable {} -extension User: @unchecked Sendable {} -extension RouteDiscovery: @unchecked Sendable {} -extension Routing: @unchecked Sendable {} -extension Routing.OneOf_Variant: @unchecked Sendable {} -extension Routing.Error: @unchecked Sendable {} -extension DataMessage: @unchecked Sendable {} -extension Waypoint: @unchecked Sendable {} -extension MqttClientProxyMessage: @unchecked Sendable {} -extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket: @unchecked Sendable {} -extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket.Priority: @unchecked Sendable {} -extension MeshPacket.Delayed: @unchecked Sendable {} -extension NodeInfo: @unchecked Sendable {} -extension MyNodeInfo: @unchecked Sendable {} -extension LogRecord: @unchecked Sendable {} -extension LogRecord.Level: @unchecked Sendable {} -extension QueueStatus: @unchecked Sendable {} -extension FromRadio: @unchecked Sendable {} -extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension ClientNotification: @unchecked Sendable {} -extension FileInfo: @unchecked Sendable {} -extension ToRadio: @unchecked Sendable {} -extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension Compressed: @unchecked Sendable {} -extension NeighborInfo: @unchecked Sendable {} -extension Neighbor: @unchecked Sendable {} -extension DeviceMetadata: @unchecked Sendable {} -extension Heartbeat: @unchecked Sendable {} -extension NodeRemoteHardwarePin: @unchecked Sendable {} -extension ChunkedPayload: @unchecked Sendable {} -extension resend_chunks: @unchecked Sendable {} -extension ChunkedPayloadResponse: @unchecked Sendable {} -extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -3241,6 +3022,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 73: .same(proto: "WIO_E5"), 74: .same(proto: "RADIOMASTER_900_BANDIT"), 75: .same(proto: "ME25LS01_4Y10TD"), + 76: .same(proto: "RP2040_FEATHER_RFM95"), + 77: .same(proto: "M5STACK_COREBASIC"), + 78: .same(proto: "M5STACK_CORE2"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3779,6 +3563,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati 6: .standard(proto: "request_id"), 7: .standard(proto: "reply_id"), 8: .same(proto: "emoji"), + 9: .same(proto: "bitfield"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3795,12 +3580,17 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati case 6: try { try decoder.decodeSingularFixed32Field(value: &self.requestID) }() case 7: try { try decoder.decodeSingularFixed32Field(value: &self.replyID) }() case 8: try { try decoder.decodeSingularFixed32Field(value: &self.emoji) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self._bitfield) }() 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.portnum != .unknownApp { try visitor.visitSingularEnumField(value: self.portnum, fieldNumber: 1) } @@ -3825,6 +3615,9 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if self.emoji != 0 { try visitor.visitSingularFixed32Field(value: self.emoji, fieldNumber: 8) } + try { if let v = self._bitfield { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3837,6 +3630,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if lhs.requestID != rhs.requestID {return false} if lhs.replyID != rhs.replyID {return false} if lhs.emoji != rhs.emoji {return false} + if lhs._bitfield != rhs._bitfield {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4153,7 +3947,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr != 0 { + if _storage._rxSnr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -4224,6 +4018,8 @@ extension MeshPacket.Priority: SwiftProtobuf._ProtoNameProviding { 10: .same(proto: "BACKGROUND"), 64: .same(proto: "DEFAULT"), 70: .same(proto: "RELIABLE"), + 80: .same(proto: "RESPONSE"), + 100: .same(proto: "HIGH"), 120: .same(proto: "ACK"), 127: .same(proto: "MAX"), ] @@ -4336,7 +4132,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -5181,7 +4977,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr != 0 { + if self.snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -5294,8 +5090,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 3186c349..d1de21cf 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +58,18 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] -} -#endif // swift(>=4.2) +} /// /// Module Config -public struct ModuleConfig { +public struct ModuleConfig: 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. @@ -218,7 +212,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -259,73 +253,11 @@ public struct ModuleConfig { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) - #if !swift(>=4.1) - public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { - // 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 (lhs, rhs) { - case (.mqtt, .mqtt): return { - guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.serial, .serial): return { - guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.externalNotification, .externalNotification): return { - guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeForward, .storeForward): return { - guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rangeTest, .rangeTest): return { - guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.telemetry, .telemetry): return { - guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.cannedMessage, .cannedMessage): return { - guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.audio, .audio): return { - guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.remoteHardware, .remoteHardware): return { - guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.neighborInfo, .neighborInfo): return { - guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.ambientLighting, .ambientLighting): return { - guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detectionSensor, .detectionSensor): return { - guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.paxcounter, .paxcounter): return { - guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// MQTT Client Config - public struct MQTTConfig { + public struct MQTTConfig: 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. @@ -400,7 +332,7 @@ public struct ModuleConfig { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings { + public struct MapReportSettings: 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. @@ -420,7 +352,7 @@ public struct ModuleConfig { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig { + public struct RemoteHardwareConfig: 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. @@ -444,7 +376,7 @@ public struct ModuleConfig { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig { + public struct NeighborInfoConfig: 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. @@ -465,7 +397,7 @@ public struct ModuleConfig { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig { + public struct DetectionSensorConfig: 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. @@ -516,7 +448,7 @@ public struct ModuleConfig { /// /// Audio Config for codec2 voice - public struct AudioConfig { + public struct AudioConfig: 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. @@ -553,7 +485,7 @@ public struct ModuleConfig { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum { + public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -600,6 +532,19 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] + } public init() {} @@ -607,7 +552,7 @@ public struct ModuleConfig { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig { + public struct PaxcounterConfig: 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. @@ -633,7 +578,7 @@ public struct ModuleConfig { /// /// Serial Config - public struct SerialConfig { + public struct SerialConfig: 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. @@ -676,7 +621,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum { + public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -744,11 +689,31 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] + } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum { + public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -793,6 +758,17 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + ] + } public init() {} @@ -800,7 +776,7 @@ public struct ModuleConfig { /// /// External Notifications Config - public struct ExternalNotificationConfig { + public struct ExternalNotificationConfig: 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. @@ -883,7 +859,7 @@ public struct ModuleConfig { /// /// Store and Forward Module Config - public struct StoreForwardConfig { + public struct StoreForwardConfig: 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. @@ -919,7 +895,7 @@ public struct ModuleConfig { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig { + public struct RangeTestConfig: 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. @@ -944,7 +920,7 @@ public struct ModuleConfig { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig { + public struct TelemetryConfig: 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. @@ -1001,7 +977,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public struct CannedMessageConfig { + public struct CannedMessageConfig: 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. @@ -1056,7 +1032,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1124,6 +1100,18 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] + } public init() {} @@ -1132,7 +1120,7 @@ public struct ModuleConfig { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig { + public struct AmbientLightingConfig: 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. @@ -1165,77 +1153,9 @@ public struct ModuleConfig { public init() {} } -#if swift(>=4.2) - -extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] -} - -extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] -} - -#endif // swift(>=4.2) - /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin { +public struct RemoteHardwarePin: 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. @@ -1257,31 +1177,6 @@ public struct RemoteHardwarePin { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RemoteHardwarePinType: @unchecked Sendable {} -extension ModuleConfig: @unchecked Sendable {} -extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} -extension ModuleConfig.MQTTConfig: @unchecked Sendable {} -extension ModuleConfig.MapReportSettings: @unchecked Sendable {} -extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} -extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} -extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} -extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} -extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} -extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} -extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} -extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} -extension RemoteHardwarePin: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index efe6cdd5..fc5e37a1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope { +public struct ServiceEnvelope: 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. @@ -57,7 +57,7 @@ public struct ServiceEnvelope { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport { +public struct MapReport: 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. @@ -121,11 +121,6 @@ public struct MapReport { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension ServiceEnvelope: @unchecked Sendable {} -extension MapReport: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index cf8aa463..f82b3c51 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount { +public struct Paxcount: 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. @@ -44,10 +44,6 @@ public struct Paxcount { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Paxcount: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index dd7e036f..46455db7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum { +public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -277,11 +277,6 @@ public enum PortNum: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -313,14 +308,9 @@ extension PortNum: CaseIterable { .atakForwarder, .max, ] + } -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PortNum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 5f51e948..9c61e6d0 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon { +public struct PowerMon: 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. @@ -31,7 +31,7 @@ public struct PowerMon { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum { + public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,37 +104,31 @@ public struct PowerMon { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerMon.State: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] -} - -#endif // swift(>=4.2) - /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage { +public struct PowerStressMessage: 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. @@ -151,7 +145,7 @@ public struct PowerStressMessage { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum { + public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -272,48 +266,35 @@ public struct PowerStressMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerStressMessage.Opcode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PowerMon: @unchecked Sendable {} -extension PowerMon.State: @unchecked Sendable {} -extension PowerStressMessage: @unchecked Sendable {} -extension PowerStressMessage.Opcode: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -323,8 +304,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -379,7 +360,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds != 0 { + if self.numSeconds.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index ac6eeb26..60f64504 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage { +public struct HardwareMessage: 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. @@ -52,7 +52,7 @@ public struct HardwareMessage { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum { + public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -110,32 +110,21 @@ public struct HardwareMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] + } public init() {} } -#if swift(>=4.2) - -extension HardwareMessage.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareMessage: @unchecked Sendable {} -extension HardwareMessage.TypeEnum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index 6fdf3208..c1f3f678 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig { +public struct RTTTLConfig: 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. @@ -36,10 +36,6 @@ public struct RTTTLConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RTTTLConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 54efa77b..0b67eaf6 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward { +public struct StoreAndForward: @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. @@ -79,7 +79,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,38 +93,12 @@ public struct StoreAndForward { /// Text from history message. case text(Data) - #if !swift(>=4.1) - public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.stats, .stats): return { - guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.history, .history): return { - guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum { + public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -242,11 +216,31 @@ public struct StoreAndForward { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] + } /// /// TODO: REPLACE - public struct Statistics { + public struct Statistics: 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. @@ -294,7 +288,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct History { + public struct History: 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. @@ -319,7 +313,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct Heartbeat { + public struct Heartbeat: 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. @@ -340,41 +334,6 @@ public struct StoreAndForward { public init() {} } -#if swift(>=4.2) - -extension StoreAndForward.RequestResponse: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension StoreAndForward: @unchecked Sendable {} -extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} -extension StoreAndForward.RequestResponse: @unchecked Sendable {} -extension StoreAndForward.Statistics: @unchecked Sendable {} -extension StoreAndForward.History: @unchecked Sendable {} -extension StoreAndForward.Heartbeat: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index e4b9ee08..44183031 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum { +public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -140,6 +140,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) case max17048 // = 28 + + /// + /// Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor + case customSensor // = 29 case UNRECOGNIZED(Int) public init() { @@ -177,6 +181,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 26: self = .bmp3Xx case 27: self = .icm20948 case 28: self = .max17048 + case 29: self = .customSensor default: self = .UNRECOGNIZED(rawValue) } } @@ -212,15 +217,11 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .bmp3Xx: return 26 case .icm20948: return 27 case .max17048: return 28 + case .customSensor: return 29 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -252,14 +253,14 @@ extension TelemetrySensorType: CaseIterable { .bmp3Xx, .icm20948, .max17048, + .customSensor, ] -} -#endif // swift(>=4.2) +} /// /// Key native device metrics such as battery level -public struct DeviceMetrics { +public struct DeviceMetrics: 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. @@ -332,7 +333,7 @@ public struct DeviceMetrics { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics { +public struct EnvironmentMetrics: @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. @@ -535,7 +536,7 @@ public struct EnvironmentMetrics { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics { +public struct PowerMetrics: 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. @@ -620,7 +621,7 @@ public struct PowerMetrics { /// /// Air quality metrics -public struct AirQualityMetrics { +public struct AirQualityMetrics: 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. @@ -777,7 +778,7 @@ public struct AirQualityMetrics { /// /// Local device mesh statistics -public struct LocalStats { +public struct LocalStats: 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. @@ -821,7 +822,7 @@ public struct LocalStats { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry { +public struct Telemetry: 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. @@ -884,7 +885,7 @@ public struct Telemetry { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -901,36 +902,6 @@ public struct Telemetry { /// Local device mesh statistics case localStats(LocalStats) - #if !swift(>=4.1) - public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { - // 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 (lhs, rhs) { - case (.deviceMetrics, .deviceMetrics): return { - guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.environmentMetrics, .environmentMetrics): return { - guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.airQualityMetrics, .airQualityMetrics): return { - guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.powerMetrics, .powerMetrics): return { - guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.localStats, .localStats): return { - guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -938,7 +909,7 @@ public struct Telemetry { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config { +public struct Nau7802Config: 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. @@ -956,18 +927,6 @@ public struct Nau7802Config { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension TelemetrySensorType: @unchecked Sendable {} -extension DeviceMetrics: @unchecked Sendable {} -extension EnvironmentMetrics: @unchecked Sendable {} -extension PowerMetrics: @unchecked Sendable {} -extension AirQualityMetrics: @unchecked Sendable {} -extension LocalStats: @unchecked Sendable {} -extension Telemetry: @unchecked Sendable {} -extension Telemetry.OneOf_Variant: @unchecked Sendable {} -extension Nau7802Config: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1003,6 +962,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 26: .same(proto: "BMP3XX"), 27: .same(proto: "ICM20948"), 28: .same(proto: "MAX17048"), + 29: .same(proto: "CUSTOM_SENSOR"), ] } @@ -1474,10 +1434,10 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.uptimeSeconds != 0 { try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) } - if self.channelUtilization != 0 { + if self.channelUtilization.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 2) } - if self.airUtilTx != 0 { + if self.airUtilTx.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 3) } if self.numPacketsTx != 0 { @@ -1666,7 +1626,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor != 0 { + if self.calibrationFactor.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 1f41fe0b..89d0097c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem { +public struct XModem: @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. @@ -35,7 +35,7 @@ public struct XModem { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum { + public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,34 +79,23 @@ public struct XModem { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] + } public init() {} } -#if swift(>=4.2) - -extension XModem.Control: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension XModem: @unchecked Sendable {} -extension XModem.Control: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/protobufs b/protobufs index b6237629..0acaec6e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit b623762940ebdb1887a3b31b86f4d9cdaa7e6ecf +Subproject commit 0acaec6eff00e748beeae89148093221f131cd9c From 042c030cf65cbc6ef01b3868d9d329c4d34971c3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Sep 2024 11:25:51 -0700 Subject: [PATCH 205/333] Update config to use new protos --- Localizable.xcstrings | 6 +- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 476 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 12 +- .../Views/Settings/Config/DeviceConfig.swift | 10 - .../Views/Settings/Config/LoRaConfig.swift | 10 + .../Settings/Config/SecurityConfig.swift | 2 +- 8 files changed, 500 insertions(+), 22 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b447989f..42554c57 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5068,9 +5068,6 @@ }, "Debug" : { - }, - "Debug Log" : { - }, "Debug Logs" : { @@ -15914,6 +15911,9 @@ }, "OK" : { + }, + "Ok to MQTT" : { + }, "OLED Type" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1e63593b..b70f45ca 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -376,6 +376,7 @@ DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = ""; }; DD77093E2AA1B146007A8BF0 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = ""; }; + DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 44.xcdatamodel"; sourceTree = ""; }; DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = ""; }; DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = ""; }; DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = ""; }; @@ -1898,6 +1899,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, @@ -1942,7 +1944,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */; + currentVersion = DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 04d3cc1a..8a88003b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 43.xcdatamodel + MeshtasticDataModelV 44.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents new file mode 100644 index 00000000..98de0347 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c1ee8470..fec3fbe9 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -463,7 +463,6 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi let newDeviceConfig = DeviceConfigEntity(context: context) newDeviceConfig.role = Int32(config.role.rawValue) newDeviceConfig.serialEnabled = config.serialEnabled - newDeviceConfig.debugLogEnabled = config.debugLogEnabled newDeviceConfig.buttonGpio = Int32(config.buttonGpio) newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) @@ -476,7 +475,6 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi } else { fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) fetchedNode[0].deviceConfig?.serialEnabled = config.serialEnabled - fetchedNode[0].deviceConfig?.debugLogEnabled = config.debugLogEnabled fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) @@ -818,21 +816,23 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s let newSecurityConfig = SecurityConfigEntity(context: context) newSecurityConfig.publicKey = config.publicKey newSecurityConfig.privateKey = config.privateKey - newSecurityConfig.adminKey = config.adminKey + if config.adminKey.count > 0 { + newSecurityConfig.adminKey = config.adminKey[0] + } newSecurityConfig.isManaged = config.isManaged newSecurityConfig.serialEnabled = config.serialEnabled newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled - newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled fetchedNode[0].securityConfig = newSecurityConfig } else { fetchedNode[0].securityConfig?.publicKey = config.publicKey fetchedNode[0].securityConfig?.privateKey = config.privateKey - fetchedNode[0].securityConfig?.adminKey = config.adminKey + if config.adminKey.count > 0 { + fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] + } fetchedNode[0].securityConfig?.isManaged = config.isManaged fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled - fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled } if sessionPasskey?.count != 0 { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index dc17446e..c78cb845 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -24,7 +24,6 @@ struct DeviceConfig: View { @State var buzzerGPIO = 0 @State var buttonGPIO = 0 @State var serialEnabled = true - @State var debugLogEnabled = false @State var rebroadcastMode = 0 @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @@ -89,10 +88,6 @@ struct DeviceConfig: View { Label("Serial Console", systemImage: "terminal") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $debugLogEnabled) { - Label("Debug Log", systemImage: "ant.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) VStack(alignment: .leading) { HStack { Label("Time Zone", systemImage: "clock.badge.exclamationmark") @@ -200,7 +195,6 @@ struct DeviceConfig: View { var dc = Config.DeviceConfig() dc.role = DeviceRoles(rawValue: deviceRole)!.protoEnumValue() dc.serialEnabled = serialEnabled - dc.debugLogEnabled = debugLogEnabled dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() @@ -259,9 +253,6 @@ struct DeviceConfig: View { .onChange(of: serialEnabled) { if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true } } - .onChange(of: debugLogEnabled) { - if $0 != node?.deviceConfig?.debugLogEnabled { hasChanges = true } - } .onChange(of: buttonGPIO) { newButtonGPIO in if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } @@ -289,7 +280,6 @@ struct DeviceConfig: View { func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) self.serialEnabled = (node?.deviceConfig?.serialEnabled ?? true) - self.debugLogEnabled = node?.deviceConfig?.debugLogEnabled ?? false self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0) self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 43729f86..13623ee3 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -45,6 +45,7 @@ struct LoRaConfig: View { @State var rxBoostedGain = false @State var overrideFrequency: Float = 0.0 @State var ignoreMqtt = false + @State var okToMqtt = false let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -100,6 +101,10 @@ struct LoRaConfig: View { Label("Ignore MQTT", systemImage: "server.rack") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $okToMqtt) { + Label("Ok to MQTT", systemImage: "network") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $txEnabled) { Label("Transmit Enabled", systemImage: "waveform.path") @@ -209,6 +214,7 @@ struct LoRaConfig: View { lc.sx126XRxBoostedGain = rxBoostedGain lc.overrideFrequency = overrideFrequency lc.ignoreMqtt = ignoreMqtt + lc.configOkToMqtt = okToMqtt if connectedNode?.num ?? -1 == node?.user?.num ?? 0 { UserDefaults.modemPreset = modemPreset } @@ -292,6 +298,9 @@ struct LoRaConfig: View { .onChange(of: ignoreMqtt) { if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true } } + .onChange(of: okToMqtt) { + if $0 != node?.loRaConfig?.okToMqtt { hasChanges = true } + } } func setLoRaValues() { self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) @@ -307,6 +316,7 @@ struct LoRaConfig: View { self.rxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0 self.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false + self.okToMqtt = node?.loRaConfig?.okToMqtt ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e58de058..95a07dfb 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -186,7 +186,7 @@ struct SecurityConfig: View { var config = Config.SecurityConfig() config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() - config.adminKey = Data(base64Encoded: adminKey) ?? Data() + config.adminKey = [Data(base64Encoded: adminKey) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled From f8e1d0bfe39c4838db19df1fa62faad36da428ab Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 12 Sep 2024 13:52:47 -0700 Subject: [PATCH 206/333] Save don't mqtt me bro --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index fec3fbe9..faa8e606 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -598,6 +598,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa newLoRaConfig.channelNum = Int32(config.channelNum) newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain newLoRaConfig.ignoreMqtt = config.ignoreMqtt + newLoRaConfig.okToMqtt = config.configOkToMqtt fetchedNode[0].loRaConfig = newLoRaConfig } else { fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue) @@ -615,6 +616,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum) fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt + fetchedNode[0].loRaConfig?.okToMqtt = config.configOkToMqtt fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain } if sessionPasskey != nil { From 37d652cb39ea3c19cda21c17dbdea184d4ac3990 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Sep 2024 08:48:09 -0700 Subject: [PATCH 207/333] Update protobufs --- .../Sources/meshtastic/admin.pb.swift | 5 + .../Sources/meshtastic/config.pb.swift | 119 ++++++++++++------ .../Sources/meshtastic/mesh.pb.swift | 65 ++++++++++ .../Sources/meshtastic/telemetry.pb.swift | 8 ++ protobufs | 2 +- 5 files changed, 161 insertions(+), 38 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 0f1c3586..53d244d1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -844,6 +844,7 @@ public struct AdminMessage { /// /// TODO: REPLACE case securityConfig // = 7 + case sessionkeyConfig // = 8 case UNRECOGNIZED(Int) public init() { @@ -860,6 +861,7 @@ public struct AdminMessage { case 5: self = .loraConfig case 6: self = .bluetoothConfig case 7: self = .securityConfig + case 8: self = .sessionkeyConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -874,6 +876,7 @@ public struct AdminMessage { case .loraConfig: return 5 case .bluetoothConfig: return 6 case .securityConfig: return 7 + case .sessionkeyConfig: return 8 case .UNRECOGNIZED(let i): return i } } @@ -998,6 +1001,7 @@ extension AdminMessage.ConfigType: CaseIterable { .loraConfig, .bluetoothConfig, .securityConfig, + .sessionkeyConfig, ] } @@ -1755,6 +1759,7 @@ extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "LORA_CONFIG"), 6: .same(proto: "BLUETOOTH_CONFIG"), 7: .same(proto: "SECURITY_CONFIG"), + 8: .same(proto: "SESSIONKEY_CONFIG"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 4b953470..37832baa 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -93,6 +93,14 @@ public struct Config { set {payloadVariant = .security(newValue)} } + public var sessionkey: Config.SessionkeyConfig { + get { + if case .sessionkey(let v)? = payloadVariant {return v} + return Config.SessionkeyConfig() + } + set {payloadVariant = .sessionkey(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -106,6 +114,7 @@ public struct Config { case lora(Config.LoRaConfig) case bluetooth(Config.BluetoothConfig) case security(Config.SecurityConfig) + case sessionkey(Config.SessionkeyConfig) #if !swift(>=4.1) public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { @@ -145,6 +154,10 @@ public struct Config { guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } return l == r }() + case (.sessionkey, .sessionkey): return { + guard case .sessionkey(let l) = lhs, case .sessionkey(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -167,12 +180,6 @@ public struct Config { /// Moved to SecurityConfig public var serialEnabled: Bool = false - /// - /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Set this to true to leave the debug log outputting even when API is active. - /// Moved to SecurityConfig - public var debugLogEnabled: Bool = false - /// /// For boards without a hard wired button, this is the pin number that will be used /// Boards that have more than one button can swap the function with this one. defaults to BUTTON_PIN if defined. @@ -1250,6 +1257,13 @@ public struct Config { set {_uniqueStorage()._ignoreMqtt = newValue} } + /// + /// Sets the ok_to_mqtt bit on outgoing packets + public var configOkToMqtt: Bool { + get {return _storage._configOkToMqtt} + set {_uniqueStorage()._configOkToMqtt = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum RegionCode: SwiftProtobuf.Enum { @@ -1492,11 +1506,6 @@ public struct Config { /// Specified PIN for PairingMode.FixedPin public var fixedPin: UInt32 = 0 - /// - /// Enables device (serial style logs) over Bluetooth - /// Moved to SecurityConfig - public var deviceLoggingEnabled: Bool = false - public var unknownFields = SwiftProtobuf.UnknownStorage() public enum PairingMode: SwiftProtobuf.Enum { @@ -1559,7 +1568,7 @@ public struct Config { /// /// The public key authorized to send admin messages to this node. - public var adminKey: Data = Data() + public var adminKey: [Data] = [] /// /// If true, device is considered to be "managed" by a mesh administrator via admin messages @@ -1572,13 +1581,9 @@ public struct Config { /// /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - /// Output live debug logging over serial. + /// Output live debug logging over serial or bluetooth is set to true. public var debugLogApiEnabled: Bool = false - /// - /// Enables device (serial style logs) over Bluetooth - public var bluetoothLoggingEnabled: Bool = false - /// /// Allow incoming device control over the insecure legacy admin channel. public var adminChannelEnabled: Bool = false @@ -1588,6 +1593,18 @@ public struct Config { public init() {} } + /// + /// Blank config request, strictly for getting the session key + public struct SessionkeyConfig { + // 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 unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + } + public init() {} } @@ -1784,6 +1801,7 @@ extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} extension Config.BluetoothConfig: @unchecked Sendable {} extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} extension Config.SecurityConfig: @unchecked Sendable {} +extension Config.SessionkeyConfig: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -1801,6 +1819,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 6: .same(proto: "lora"), 7: .same(proto: "bluetooth"), 8: .same(proto: "security"), + 9: .same(proto: "sessionkey"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1913,6 +1932,19 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas self.payloadVariant = .security(v) } }() + case 9: try { + var v: Config.SessionkeyConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .sessionkey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .sessionkey(v) + } + }() default: break } } @@ -1956,6 +1988,10 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas guard case .security(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 8) }() + case .sessionkey?: try { + guard case .sessionkey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1973,7 +2009,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "role"), 2: .standard(proto: "serial_enabled"), - 3: .standard(proto: "debug_log_enabled"), 4: .standard(proto: "button_gpio"), 5: .standard(proto: "buzzer_gpio"), 6: .standard(proto: "rebroadcast_mode"), @@ -1993,7 +2028,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl switch fieldNumber { case 1: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 2: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.debugLogEnabled) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self.buttonGpio) }() case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }() @@ -2015,9 +2049,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.serialEnabled != false { try visitor.visitSingularBoolField(value: self.serialEnabled, fieldNumber: 2) } - if self.debugLogEnabled != false { - try visitor.visitSingularBoolField(value: self.debugLogEnabled, fieldNumber: 3) - } if self.buttonGpio != 0 { try visitor.visitSingularUInt32Field(value: self.buttonGpio, fieldNumber: 4) } @@ -2051,7 +2082,6 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl public static func ==(lhs: Config.DeviceConfig, rhs: Config.DeviceConfig) -> Bool { if lhs.role != rhs.role {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} - if lhs.debugLogEnabled != rhs.debugLogEnabled {return false} if lhs.buttonGpio != rhs.buttonGpio {return false} if lhs.buzzerGpio != rhs.buzzerGpio {return false} if lhs.rebroadcastMode != rhs.rebroadcastMode {return false} @@ -2595,6 +2625,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 15: .standard(proto: "pa_fan_disabled"), 103: .standard(proto: "ignore_incoming"), 104: .standard(proto: "ignore_mqtt"), + 105: .standard(proto: "config_ok_to_mqtt"), ] fileprivate class _StorageClass { @@ -2615,6 +2646,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _paFanDisabled: Bool = false var _ignoreIncoming: [UInt32] = [] var _ignoreMqtt: Bool = false + var _configOkToMqtt: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -2646,6 +2678,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem _paFanDisabled = source._paFanDisabled _ignoreIncoming = source._ignoreIncoming _ignoreMqtt = source._ignoreMqtt + _configOkToMqtt = source._configOkToMqtt } } @@ -2681,6 +2714,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem case 15: try { try decoder.decodeSingularBoolField(value: &_storage._paFanDisabled) }() case 103: try { try decoder.decodeRepeatedUInt32Field(value: &_storage._ignoreIncoming) }() case 104: try { try decoder.decodeSingularBoolField(value: &_storage._ignoreMqtt) }() + case 105: try { try decoder.decodeSingularBoolField(value: &_storage._configOkToMqtt) }() default: break } } @@ -2740,6 +2774,9 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._ignoreMqtt != false { try visitor.visitSingularBoolField(value: _storage._ignoreMqtt, fieldNumber: 104) } + if _storage._configOkToMqtt != false { + try visitor.visitSingularBoolField(value: _storage._configOkToMqtt, fieldNumber: 105) + } } try unknownFields.traverse(visitor: &visitor) } @@ -2766,6 +2803,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._paFanDisabled != rhs_storage._paFanDisabled {return false} if _storage._ignoreIncoming != rhs_storage._ignoreIncoming {return false} if _storage._ignoreMqtt != rhs_storage._ignoreMqtt {return false} + if _storage._configOkToMqtt != rhs_storage._configOkToMqtt {return false} return true } if !storagesAreEqual {return false} @@ -2819,7 +2857,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI 1: .same(proto: "enabled"), 2: .same(proto: "mode"), 3: .standard(proto: "fixed_pin"), - 4: .standard(proto: "device_logging_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2831,7 +2868,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }() case 2: try { try decoder.decodeSingularEnumField(value: &self.mode) }() case 3: try { try decoder.decodeSingularUInt32Field(value: &self.fixedPin) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.deviceLoggingEnabled) }() default: break } } @@ -2847,9 +2883,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if self.fixedPin != 0 { try visitor.visitSingularUInt32Field(value: self.fixedPin, fieldNumber: 3) } - if self.deviceLoggingEnabled != false { - try visitor.visitSingularBoolField(value: self.deviceLoggingEnabled, fieldNumber: 4) - } try unknownFields.traverse(visitor: &visitor) } @@ -2857,7 +2890,6 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if lhs.enabled != rhs.enabled {return false} if lhs.mode != rhs.mode {return false} if lhs.fixedPin != rhs.fixedPin {return false} - if lhs.deviceLoggingEnabled != rhs.deviceLoggingEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2880,7 +2912,6 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 4: .standard(proto: "is_managed"), 5: .standard(proto: "serial_enabled"), 6: .standard(proto: "debug_log_api_enabled"), - 7: .standard(proto: "bluetooth_logging_enabled"), 8: .standard(proto: "admin_channel_enabled"), ] @@ -2892,11 +2923,10 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm switch fieldNumber { case 1: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() case 2: try { try decoder.decodeSingularBytesField(value: &self.privateKey) }() - case 3: try { try decoder.decodeSingularBytesField(value: &self.adminKey) }() + case 3: try { try decoder.decodeRepeatedBytesField(value: &self.adminKey) }() case 4: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() case 5: try { try decoder.decodeSingularBoolField(value: &self.serialEnabled) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.debugLogApiEnabled) }() - case 7: try { try decoder.decodeSingularBoolField(value: &self.bluetoothLoggingEnabled) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.adminChannelEnabled) }() default: break } @@ -2911,7 +2941,7 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm try visitor.visitSingularBytesField(value: self.privateKey, fieldNumber: 2) } if !self.adminKey.isEmpty { - try visitor.visitSingularBytesField(value: self.adminKey, fieldNumber: 3) + try visitor.visitRepeatedBytesField(value: self.adminKey, fieldNumber: 3) } if self.isManaged != false { try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 4) @@ -2922,9 +2952,6 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.debugLogApiEnabled != false { try visitor.visitSingularBoolField(value: self.debugLogApiEnabled, fieldNumber: 6) } - if self.bluetoothLoggingEnabled != false { - try visitor.visitSingularBoolField(value: self.bluetoothLoggingEnabled, fieldNumber: 7) - } if self.adminChannelEnabled != false { try visitor.visitSingularBoolField(value: self.adminChannelEnabled, fieldNumber: 8) } @@ -2938,9 +2965,27 @@ extension Config.SecurityConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.isManaged != rhs.isManaged {return false} if lhs.serialEnabled != rhs.serialEnabled {return false} if lhs.debugLogApiEnabled != rhs.debugLogApiEnabled {return false} - if lhs.bluetoothLoggingEnabled != rhs.bluetoothLoggingEnabled {return false} if lhs.adminChannelEnabled != rhs.adminChannelEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } + +extension Config.SessionkeyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = Config.protoMessageName + ".SessionkeyConfig" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Config.SessionkeyConfig, rhs: Config.SessionkeyConfig) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index ba12908d..c7e8dc07 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -346,6 +346,19 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. case me25Ls014Y10Td // = 75 + /// + /// RP2040_FEATHER_RFM95 + /// Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED + /// https://www.adafruit.com/product/5714 + /// https://www.adafruit.com/product/326 + /// https://www.adafruit.com/product/938 + /// ^^^ short A0 to switch to I2C address 0x3C + case rp2040FeatherRfm95 // = 76 + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + case m5StackCorebasic // = 77 + case m5StackCore2 // = 78 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -434,6 +447,9 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 73: self = .wioE5 case 74: self = .radiomaster900Bandit case 75: self = .me25Ls014Y10Td + case 76: self = .rp2040FeatherRfm95 + case 77: self = .m5StackCorebasic + case 78: self = .m5StackCore2 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -516,6 +532,9 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .wioE5: return 73 case .radiomaster900Bandit: return 74 case .me25Ls014Y10Td: return 75 + case .rp2040FeatherRfm95: return 76 + case .m5StackCorebasic: return 77 + case .m5StackCore2: return 78 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -603,6 +622,9 @@ extension HardwareModel: CaseIterable { .wioE5, .radiomaster900Bandit, .me25Ls014Y10Td, + .rp2040FeatherRfm95, + .m5StackCorebasic, + .m5StackCore2, .privateHw, ] } @@ -1520,9 +1542,22 @@ public struct DataMessage { /// a message a heart or poop emoji. public var emoji: UInt32 = 0 + /// + /// Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. + public var bitfield: UInt32 { + get {return _bitfield ?? 0} + set {_bitfield = newValue} + } + /// Returns true if `bitfield` has been explicitly set. + public var hasBitfield: Bool {return self._bitfield != nil} + /// Clears the value of `bitfield`. Subsequent reads from it will return its default value. + public mutating func clearBitfield() {self._bitfield = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _bitfield: UInt32? = nil } /// @@ -1907,6 +1942,15 @@ public struct MeshPacket { /// assume it is important and use a slightly higher priority case reliable // = 70 + /// + /// If priority is unset but the packet is a response to a request, we want it to get there relatively quickly. + /// Furthermore, responses stop relaying packets directed to a node early. + case response // = 80 + + /// + /// Higher priority for specific message types (portnums) to distinguish between other reliable packets. + case high // = 100 + /// /// Ack/naks are sent with very high priority to ensure that retransmission /// stops as soon as possible @@ -1928,6 +1972,8 @@ public struct MeshPacket { case 10: self = .background case 64: self = .default case 70: self = .reliable + case 80: self = .response + case 100: self = .high case 120: self = .ack case 127: self = .max default: self = .UNRECOGNIZED(rawValue) @@ -1941,6 +1987,8 @@ public struct MeshPacket { case .background: return 10 case .default: return 64 case .reliable: return 70 + case .response: return 80 + case .high: return 100 case .ack: return 120 case .max: return 127 case .UNRECOGNIZED(let i): return i @@ -2006,6 +2054,8 @@ extension MeshPacket.Priority: CaseIterable { .background, .default, .reliable, + .response, + .high, .ack, .max, ] @@ -3241,6 +3291,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 73: .same(proto: "WIO_E5"), 74: .same(proto: "RADIOMASTER_900_BANDIT"), 75: .same(proto: "ME25LS01_4Y10TD"), + 76: .same(proto: "RP2040_FEATHER_RFM95"), + 77: .same(proto: "M5STACK_COREBASIC"), + 78: .same(proto: "M5STACK_CORE2"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3779,6 +3832,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati 6: .standard(proto: "request_id"), 7: .standard(proto: "reply_id"), 8: .same(proto: "emoji"), + 9: .same(proto: "bitfield"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3795,12 +3849,17 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati case 6: try { try decoder.decodeSingularFixed32Field(value: &self.requestID) }() case 7: try { try decoder.decodeSingularFixed32Field(value: &self.replyID) }() case 8: try { try decoder.decodeSingularFixed32Field(value: &self.emoji) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self._bitfield) }() 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.portnum != .unknownApp { try visitor.visitSingularEnumField(value: self.portnum, fieldNumber: 1) } @@ -3825,6 +3884,9 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if self.emoji != 0 { try visitor.visitSingularFixed32Field(value: self.emoji, fieldNumber: 8) } + try { if let v = self._bitfield { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3837,6 +3899,7 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if lhs.requestID != rhs.requestID {return false} if lhs.replyID != rhs.replyID {return false} if lhs.emoji != rhs.emoji {return false} + if lhs._bitfield != rhs._bitfield {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4224,6 +4287,8 @@ extension MeshPacket.Priority: SwiftProtobuf._ProtoNameProviding { 10: .same(proto: "BACKGROUND"), 64: .same(proto: "DEFAULT"), 70: .same(proto: "RELIABLE"), + 80: .same(proto: "RESPONSE"), + 100: .same(proto: "HIGH"), 120: .same(proto: "ACK"), 127: .same(proto: "MAX"), ] diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index e4b9ee08..dc1d6cce 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -140,6 +140,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) case max17048 // = 28 + + /// + /// Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor + case customSensor // = 29 case UNRECOGNIZED(Int) public init() { @@ -177,6 +181,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 26: self = .bmp3Xx case 27: self = .icm20948 case 28: self = .max17048 + case 29: self = .customSensor default: self = .UNRECOGNIZED(rawValue) } } @@ -212,6 +217,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .bmp3Xx: return 26 case .icm20948: return 27 case .max17048: return 28 + case .customSensor: return 29 case .UNRECOGNIZED(let i): return i } } @@ -252,6 +258,7 @@ extension TelemetrySensorType: CaseIterable { .bmp3Xx, .icm20948, .max17048, + .customSensor, ] } @@ -1003,6 +1010,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 26: .same(proto: "BMP3XX"), 27: .same(proto: "ICM20948"), 28: .same(proto: "MAX17048"), + 29: .same(proto: "CUSTOM_SENSOR"), ] } diff --git a/protobufs b/protobufs index 4da558d0..0acaec6e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4da558d0f73c46ef91b74431facee73c09affbfc +Subproject commit 0acaec6eff00e748beeae89148093221f131cd9c From 5fd1a821b3577dbde0400d22af349f4f5788d9b5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 14 Sep 2024 10:37:00 -0700 Subject: [PATCH 208/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b70f45ca..7d9209f1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1687,7 +1687,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.4; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1722,7 +1722,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.4; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1754,7 +1754,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.4; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1787,7 +1787,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.4; + MARKETING_VERSION = 2.5.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 98c90d52900a51cf05cc6fc22ca30266fd37d542 Mon Sep 17 00:00:00 2001 From: Jacob Powers Date: Sat, 14 Sep 2024 19:25:40 +0000 Subject: [PATCH 209/333] fix signal quality display when rssi missing --- .../Helpers/LoRaSignalStrengthIndicator.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift index 4405e819..03735ae2 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift @@ -72,6 +72,20 @@ private func getColor(signalStrength: LoRaSignalStrength) -> Color { } func getLoRaSignalStrength(snr: Float, rssi: Int32, preset: ModemPresets) -> LoRaSignalStrength { + // rssi is 0 when not available + if rssi == 0 { + if snr > (preset.snrLimit()) { + return .good + } + if snr < (preset.snrLimit() - 7.5) { + return .none + } + if snr <= (preset.snrLimit() - 5.5) { + return .bad + } + return .fair + } + if rssi > -115 && snr > (preset.snrLimit()) { return .good } else if rssi < -126 && snr < (preset.snrLimit() - 7.5) { From 70b9a8de513f85455f86d4231cbff27b852fca27 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 15 Sep 2024 11:35:35 -0700 Subject: [PATCH 210/333] Better filter before setting mismatched key --- Meshtastic/Helpers/MeshPackets.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 0c270ac9..09683c0e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -886,11 +886,12 @@ func textMessageAppPacket( if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) { - // We have a key, check if it matches + if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) && newMessage.toUser != nil && packet.pkiEncrypted { + // We have a key and it is a PKC encrypted DM, check if it matches if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false newMessage.fromUser?.newPublicKey = newMessage.publicKey + Logger.data.error("🔑 Key Mismatch origninal key: \(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key") new key: \(newMessage.fromUser?.newPublicKey?.base64EncodedString() ?? "No Key") ") } } else { /// We have no key, set it if it is not empty From 5a1818d5032dd671ecac4773c06b9d8c5b094361 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Sep 2024 14:15:33 -0700 Subject: [PATCH 211/333] Set message size to 200 bytes, add extra pki check before setting key --- Localizable.xcstrings | 2 +- Meshtastic/AppIntents/MessageChannelIntent.swift | 4 ++-- Meshtastic/Helpers/MeshPackets.swift | 6 ++++-- .../Views/Messages/TextMessageField/TextMessageField.swift | 2 +- .../Views/Messages/TextMessageField/TextMessageSize.swift | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a61ea1cf..d91dded2 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -14559,7 +14559,7 @@ "Message" : { }, - "Message content exceeds 228 bytes." : { + "Message content exceeds 200 bytes." : { }, "Message Status Options" : { diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index 29d0a6c2..6001d957 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -38,8 +38,8 @@ struct MessageChannelIntent: AppIntent { throw AppIntentErrors.AppIntentError.message("Failed to encode message content") } - if messageData.count > 228 { - throw $messageContent.needsValueError("Message content exceeds 228 bytes.") + if messageData.count > 200 { + throw $messageContent.needsValueError("Message content exceeds 200 bytes.") } if(!BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0)){ diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 09683c0e..a75a4c04 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -891,9 +891,11 @@ func textMessageAppPacket( if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false newMessage.fromUser?.newPublicKey = newMessage.publicKey - Logger.data.error("🔑 Key Mismatch origninal key: \(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key") new key: \(newMessage.fromUser?.newPublicKey?.base64EncodedString() ?? "No Key") ") + let nodeKey = String(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key").prefix(8) + let messageKey = String(newMessage.fromUser?.newPublicKey?.base64EncodedString() ?? "No Key").prefix(8) + Logger.data.error("🔑 Key Mismatch origninal key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") } - } else { + } else if packet.pkiEncrypted { /// We have no key, set it if it is not empty if !packet.publicKey.isEmpty { newMessage.fromUser?.pkiEncrypted = true diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index 469e57a4..12b72ae9 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -2,7 +2,7 @@ import SwiftUI import OSLog struct TextMessageField: View { - static let maxbytes = 228 + static let maxbytes = 200 @EnvironmentObject var bleManager: BLEManager let destination: MessageDestination diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index 2b7b1e5e..c939b825 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -15,6 +15,6 @@ struct TextMessageSize: View { struct TextMessageSizePreview: PreviewProvider { static var previews: some View { - TextMessageSize(maxbytes: 228, totalBytes: 100) + TextMessageSize(maxbytes: 200, totalBytes: 100) } } From c5f847a5488e0513c4dd8218109a189863fc15a0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 17 Sep 2024 14:33:30 -0700 Subject: [PATCH 212/333] Bump bersion --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7d9209f1..353226be 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1687,7 +1687,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.5; + MARKETING_VERSION = 2.5.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1722,7 +1722,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.5; + MARKETING_VERSION = 2.5.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1754,7 +1754,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.5; + MARKETING_VERSION = 2.5.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1787,7 +1787,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.5; + MARKETING_VERSION = 2.5.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From aaa6bbbfd29ed5012373af94f18c5399c0b790d8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Sep 2024 16:36:06 -0700 Subject: [PATCH 213/333] Log right public key field --- Localizable.xcstrings | 18 +++++++++++++++--- Meshtastic/Helpers/MeshPackets.swift | 9 +-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d91dded2..0ea54454 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -21,6 +21,21 @@ }, ": %d" : { + }, + ".dot" : { + + }, + ".gauge" : { + + }, + ".gradient" : { + + }, + ".pill" : { + + }, + ".text" : { + }, "(Re)define PIN_GPS_EN for your board." : { @@ -19154,9 +19169,6 @@ }, "Send" : { - }, - "Send ${messageContent} to ${channelNumber}" : { - }, "Send a Group Message" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index a75a4c04..badac8ec 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -872,27 +872,23 @@ func textMessageAppPacket( newMessage.read = true } } - if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } - if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum { if !storeForwardBroadcast { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) } } - if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) && newMessage.toUser != nil && packet.pkiEncrypted { // We have a key and it is a PKC encrypted DM, check if it matches if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false newMessage.fromUser?.newPublicKey = newMessage.publicKey let nodeKey = String(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key").prefix(8) - let messageKey = String(newMessage.fromUser?.newPublicKey?.base64EncodedString() ?? "No Key").prefix(8) + let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8) Logger.data.error("🔑 Key Mismatch origninal key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") } } else if packet.pkiEncrypted { @@ -902,17 +898,14 @@ func textMessageAppPacket( newMessage.fromUser?.publicKey = packet.publicKey } } - if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) } else { newMessage.fromUser?.userNode?.lastHeard = Date() } } - newMessage.messagePayload = messageText newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!) - if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil { newMessage.fromUser?.lastMessage = Date() } From 94708e76cba86c1109bc4c5937444fc927b5019b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Sep 2024 18:25:20 -0700 Subject: [PATCH 214/333] fix logging typo, update lock icon on messages to only show for real acks --- Meshtastic/Helpers/MeshPackets.swift | 6 ++---- Meshtastic/Views/Messages/MessageText.swift | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index badac8ec..96b14cb0 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -883,13 +883,13 @@ func textMessageAppPacket( if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) && newMessage.toUser != nil && packet.pkiEncrypted { - // We have a key and it is a PKC encrypted DM, check if it matches + /// We have a key and it is a PKC encrypted DM, check if it matches if newMessage.fromUser?.publicKey != newMessage.publicKey { newMessage.fromUser?.keyMatch = false newMessage.fromUser?.newPublicKey = newMessage.publicKey let nodeKey = String(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key").prefix(8) let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8) - Logger.data.error("🔑 Key Mismatch origninal key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") + Logger.data.error("🔑 Key mismatch original key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") } } else if packet.pkiEncrypted { /// We have no key, set it if it is not empty @@ -909,9 +909,7 @@ func textMessageAppPacket( if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil { newMessage.fromUser?.lastMessage = Date() } - var messageSaved = false - do { try context.save() Logger.data.info("💾 Saved a new message for \(newMessage.messageId)") diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 93f8cf25..1f2cc017 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -30,7 +30,7 @@ struct MessageText: View { .background(isCurrentUser ? .accentColor : Color(.gray)) .cornerRadius(15) .overlay { - if message.pkiEncrypted { + if message.pkiEncrypted && message.ackError == 0 && message.realACK { VStack(alignment: .trailing) { Spacer() HStack { From 470f3432da4386c18ac61cee2270e92d1ff5962f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 18 Sep 2024 18:50:46 -0700 Subject: [PATCH 215/333] Clean up key mis match logic --- Meshtastic/Helpers/MeshPackets.swift | 34 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 96b14cb0..27c41aeb 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -827,7 +827,7 @@ func textMessageAppPacket( } } let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false - + if !wantRangeTestPackets && rangeTest { return } @@ -840,16 +840,13 @@ func textMessageAppPacket( } } } - + if messageText?.count ?? 0 > 0 { MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") - let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) - do { let fetchedUsers = try context.fetch(messageUsers) - let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) if packet.rxTime > 0 { @@ -863,10 +860,6 @@ func textMessageAppPacket( newMessage.isEmoji = packet.decoded.emoji == 1 newMessage.channel = Int32(packet.channel) newMessage.portNum = Int32(packet.decoded.portnum.rawValue) - if newMessage.toUser?.pkiEncrypted ?? false { - newMessage.pkiEncrypted = true - newMessage.publicKey = packet.publicKey - } if packet.decoded.portnum == PortNum.detectionSensorApp { if !UserDefaults.enableDetectionNotifications { newMessage.read = true @@ -882,14 +875,21 @@ func textMessageAppPacket( } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) && newMessage.toUser != nil && packet.pkiEncrypted { - /// We have a key and it is a PKC encrypted DM, check if it matches - if newMessage.fromUser?.publicKey != newMessage.publicKey { - newMessage.fromUser?.keyMatch = false - newMessage.fromUser?.newPublicKey = newMessage.publicKey - let nodeKey = String(newMessage.fromUser?.publicKey?.base64EncodedString() ?? "No Key").prefix(8) - let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8) - Logger.data.error("🔑 Key mismatch original key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") + /// Set the public key for the message + if newMessage.fromUser?.pkiEncrypted ?? false { + newMessage.pkiEncrypted = true + newMessage.publicKey = packet.publicKey + } + /// Check for key mismatch + if let nodeKey = newMessage.fromUser?.publicKey { + if newMessage.toUser != nil && packet.pkiEncrypted && !packet.publicKey.isEmpty { + if nodeKey != newMessage.publicKey { + newMessage.fromUser?.keyMatch = false + newMessage.fromUser?.newPublicKey = newMessage.publicKey + let nodeKey = String(nodeKey.base64EncodedString()).prefix(8) + let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8) + Logger.data.error("🔑 Key mismatch original key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") + } } } else if packet.pkiEncrypted { /// We have no key, set it if it is not empty From 82e75ac6b5303faf965b1a0f4d27989d449f6e3d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Sep 2024 11:20:09 -0700 Subject: [PATCH 216/333] retry on unknown pub key --- Meshtastic/Enums/RoutingError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 8b50de89..aa2a0c10 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -97,7 +97,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .pkiFailed: return true case .pkiUnknownPubkey: - return false + return true } } func protoEnumValue() -> Routing.Error { From 799f5873148230f32a729b89bbbdd2bbeb71b429 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Sep 2024 11:23:02 -0700 Subject: [PATCH 217/333] Update protos --- .../Sources/meshtastic/atak.pb.swift | 32 +++++++++++ .../Sources/meshtastic/clientonly.pb.swift | 54 +++++++++++++++++++ .../Sources/meshtastic/deviceonly.pb.swift | 14 +++-- .../Sources/meshtastic/mesh.pb.swift | 40 ++++++++++++-- protobufs | 2 +- 5 files changed, 131 insertions(+), 11 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 4406deb3..c756d94d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -322,6 +322,17 @@ public struct TAKPacket { set {payloadVariant = .chat(newValue)} } + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender + public var detail: Data { + get { + if case .detail(let v)? = payloadVariant {return v} + return Data() + } + set {payloadVariant = .detail(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -333,6 +344,10 @@ public struct TAKPacket { /// /// ATAK GeoChat message case chat(GeoChat) + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender + case detail(Data) #if !swift(>=4.1) public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { @@ -348,6 +363,10 @@ public struct TAKPacket { guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } return l == r }() + case (.detail, .detail): return { + guard case .detail(let l) = lhs, case .detail(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -555,6 +574,7 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 4: .same(proto: "status"), 5: .same(proto: "pli"), 6: .same(proto: "chat"), + 7: .same(proto: "detail"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -593,6 +613,14 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.payloadVariant = .chat(v) } }() + case 7: try { + var v: Data? + try decoder.decodeSingularBytesField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .detail(v) + } + }() default: break } } @@ -624,6 +652,10 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .chat(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 6) }() + case .detail?: try { + guard case .detail(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBytesField(value: v, fieldNumber: 7) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index c3d93bf7..f89a8e3c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -83,6 +83,39 @@ public struct DeviceProfile { /// Clears the value of `moduleConfig`. Subsequent reads from it will return its default value. public mutating func clearModuleConfig() {self._moduleConfig = nil} + /// + /// Fixed position data + public var fixedPosition: Position { + get {return _fixedPosition ?? Position()} + set {_fixedPosition = newValue} + } + /// Returns true if `fixedPosition` has been explicitly set. + public var hasFixedPosition: Bool {return self._fixedPosition != nil} + /// Clears the value of `fixedPosition`. Subsequent reads from it will return its default value. + public mutating func clearFixedPosition() {self._fixedPosition = nil} + + /// + /// Ringtone for ExternalNotification + public var ringtone: String { + get {return _ringtone ?? String()} + set {_ringtone = newValue} + } + /// Returns true if `ringtone` has been explicitly set. + public var hasRingtone: Bool {return self._ringtone != nil} + /// Clears the value of `ringtone`. Subsequent reads from it will return its default value. + public mutating func clearRingtone() {self._ringtone = nil} + + /// + /// Predefined messages for CannedMessage + public var cannedMessages: String { + get {return _cannedMessages ?? String()} + set {_cannedMessages = newValue} + } + /// Returns true if `cannedMessages` has been explicitly set. + public var hasCannedMessages: Bool {return self._cannedMessages != nil} + /// Clears the value of `cannedMessages`. Subsequent reads from it will return its default value. + public mutating func clearCannedMessages() {self._cannedMessages = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -92,6 +125,9 @@ public struct DeviceProfile { fileprivate var _channelURL: String? = nil fileprivate var _config: LocalConfig? = nil fileprivate var _moduleConfig: LocalModuleConfig? = nil + fileprivate var _fixedPosition: Position? = nil + fileprivate var _ringtone: String? = nil + fileprivate var _cannedMessages: String? = nil } #if swift(>=5.5) && canImport(_Concurrency) @@ -110,6 +146,9 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa 3: .standard(proto: "channel_url"), 4: .same(proto: "config"), 5: .standard(proto: "module_config"), + 6: .standard(proto: "fixed_position"), + 7: .same(proto: "ringtone"), + 8: .standard(proto: "canned_messages"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -123,6 +162,9 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa case 3: try { try decoder.decodeSingularStringField(value: &self._channelURL) }() case 4: try { try decoder.decodeSingularMessageField(value: &self._config) }() case 5: try { try decoder.decodeSingularMessageField(value: &self._moduleConfig) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._fixedPosition) }() + case 7: try { try decoder.decodeSingularStringField(value: &self._ringtone) }() + case 8: try { try decoder.decodeSingularStringField(value: &self._cannedMessages) }() default: break } } @@ -148,6 +190,15 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa try { if let v = self._moduleConfig { try visitor.visitSingularMessageField(value: v, fieldNumber: 5) } }() + try { if let v = self._fixedPosition { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + try { if let v = self._ringtone { + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + } }() + try { if let v = self._cannedMessages { + try visitor.visitSingularStringField(value: v, fieldNumber: 8) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -157,6 +208,9 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if lhs._channelURL != rhs._channelURL {return false} if lhs._config != rhs._config {return false} if lhs._moduleConfig != rhs._moduleConfig {return false} + if lhs._fixedPosition != rhs._fixedPosition {return false} + if lhs._ringtone != rhs._ringtone {return false} + if lhs._cannedMessages != rhs._cannedMessages {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 43506399..3dd965f2 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -235,9 +235,13 @@ public struct NodeInfoLite { /// /// Number of hops away from us this node is (0 if adjacent) public var hopsAway: UInt32 { - get {return _storage._hopsAway} + get {return _storage._hopsAway ?? 0} set {_uniqueStorage()._hopsAway = newValue} } + /// Returns true if `hopsAway` has been explicitly set. + public var hasHopsAway: Bool {return _storage._hopsAway != nil} + /// Clears the value of `hopsAway`. Subsequent reads from it will return its default value. + public mutating func clearHopsAway() {_uniqueStorage()._hopsAway = nil} /// /// True if node is in our favorites list @@ -620,7 +624,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _deviceMetrics: DeviceMetrics? = nil var _channel: UInt32 = 0 var _viaMqtt: Bool = false - var _hopsAway: UInt32 = 0 + var _hopsAway: UInt32? = nil var _isFavorite: Bool = false #if swift(>=5.10) @@ -710,9 +714,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._viaMqtt != false { try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 8) } - if _storage._hopsAway != 0 { - try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) - } + try { if let v = _storage._hopsAway { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() if _storage._isFavorite != false { try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index c7e8dc07..6bddec60 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1416,6 +1416,14 @@ public struct Routing { /// /// The receiving node does not have a Public Key to decode with case pkiUnknownPubkey // = 35 + + /// + /// Admin packet otherwise checks out, but uses a bogus or expired session key + case adminBadSessionKey // = 36 + + /// + /// Admin packet sent using PKC, but not from a public key on the admin key list + case adminPublicKeyUnauthorized // = 37 case UNRECOGNIZED(Int) public init() { @@ -1438,6 +1446,8 @@ public struct Routing { case 33: self = .notAuthorized case 34: self = .pkiFailed case 35: self = .pkiUnknownPubkey + case 36: self = .adminBadSessionKey + case 37: self = .adminPublicKeyUnauthorized default: self = .UNRECOGNIZED(rawValue) } } @@ -1458,6 +1468,8 @@ public struct Routing { case .notAuthorized: return 33 case .pkiFailed: return 34 case .pkiUnknownPubkey: return 35 + case .adminBadSessionKey: return 36 + case .adminPublicKeyUnauthorized: return 37 case .UNRECOGNIZED(let i): return i } } @@ -1486,6 +1498,8 @@ extension Routing.Error: CaseIterable { .notAuthorized, .pkiFailed, .pkiUnknownPubkey, + .adminBadSessionKey, + .adminPublicKeyUnauthorized, ] } @@ -2167,9 +2181,13 @@ public struct NodeInfo { /// /// Number of hops away from us this node is (0 if adjacent) public var hopsAway: UInt32 { - get {return _storage._hopsAway} + get {return _storage._hopsAway ?? 0} set {_uniqueStorage()._hopsAway = newValue} } + /// Returns true if `hopsAway` has been explicitly set. + public var hasHopsAway: Bool {return _storage._hopsAway != nil} + /// Clears the value of `hopsAway`. Subsequent reads from it will return its default value. + public mutating func clearHopsAway() {_uniqueStorage()._hopsAway = nil} /// /// True if node is in our favorites list @@ -2997,6 +3015,10 @@ public struct DeviceMetadata { /// Has Remote Hardware enabled public var hasRemoteHardware_p: Bool = false + /// + /// Has PKC capabilities + public var hasPkc_p: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -3818,6 +3840,8 @@ extension Routing.Error: SwiftProtobuf._ProtoNameProviding { 33: .same(proto: "NOT_AUTHORIZED"), 34: .same(proto: "PKI_FAILED"), 35: .same(proto: "PKI_UNKNOWN_PUBKEY"), + 36: .same(proto: "ADMIN_BAD_SESSION_KEY"), + 37: .same(proto: "ADMIN_PUBLIC_KEY_UNAUTHORIZED"), ] } @@ -4326,7 +4350,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _deviceMetrics: DeviceMetrics? = nil var _channel: UInt32 = 0 var _viaMqtt: Bool = false - var _hopsAway: UInt32 = 0 + var _hopsAway: UInt32? = nil var _isFavorite: Bool = false #if swift(>=5.10) @@ -4416,9 +4440,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._viaMqtt != false { try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 8) } - if _storage._hopsAway != 0 { - try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) - } + try { if let v = _storage._hopsAway { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() if _storage._isFavorite != false { try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) } @@ -5281,6 +5305,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 8: .standard(proto: "position_flags"), 9: .standard(proto: "hw_model"), 10: .same(proto: "hasRemoteHardware"), + 11: .same(proto: "hasPKC"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -5299,6 +5324,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.hasRemoteHardware_p) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.hasPkc_p) }() default: break } } @@ -5335,6 +5361,9 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.hasRemoteHardware_p != false { try visitor.visitSingularBoolField(value: self.hasRemoteHardware_p, fieldNumber: 10) } + if self.hasPkc_p != false { + try visitor.visitSingularBoolField(value: self.hasPkc_p, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -5349,6 +5378,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if lhs.positionFlags != rhs.positionFlags {return false} if lhs.hwModel != rhs.hwModel {return false} if lhs.hasRemoteHardware_p != rhs.hasRemoteHardware_p {return false} + if lhs.hasPkc_p != rhs.hasPkc_p {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index 0acaec6e..5709c0a0 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 0acaec6eff00e748beeae89148093221f131cd9c +Subproject commit 5709c0a05eaefccbc9cb8ed3917adbf5fd134197 From 39a456954bd550ab446d37dfc50eba093abcb362 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Sep 2024 11:51:13 -0700 Subject: [PATCH 218/333] add new routing error messages --- Meshtastic/Enums/RoutingError.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index aa2a0c10..b3962a09 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -24,6 +24,8 @@ enum RoutingError: Int, CaseIterable, Identifiable { case notAuthorized = 33 case pkiFailed = 34 case pkiUnknownPubkey = 35 + case adminBadSessionKey = 36 + case adminPublicKeyUnauthorized = 37 var id: Int { self.rawValue } var display: String { @@ -57,6 +59,10 @@ enum RoutingError: Int, CaseIterable, Identifiable { return "routing.pkifailed".localized case .pkiUnknownPubkey: return "routing.pkiunknownpubkey".localized + case .adminBadSessionKey: + return "routing.adminbadsessionkey".localized + case .adminPublicKeyUnauthorized: + return "routing.adminpublickeyunauthorized".localized } } var color: Color { @@ -98,6 +104,10 @@ enum RoutingError: Int, CaseIterable, Identifiable { return true case .pkiUnknownPubkey: return true + case .adminBadSessionKey: + return true + case .adminPublicKeyUnauthorized: + return true } } func protoEnumValue() -> Routing.Error { @@ -132,6 +142,10 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Routing.Error.pkiFailed case .pkiUnknownPubkey: return Routing.Error.pkiUnknownPubkey + case .adminBadSessionKey: + return Routing.Error.adminBadSessionKey + case .adminPublicKeyUnauthorized: + return Routing.Error.adminPublicKeyUnauthorized } } } From 496016c6fa1da51955bb9664d6b73d3746d963e8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Sep 2024 20:01:05 -0700 Subject: [PATCH 219/333] Use first appear on mesh map --- Localizable.xcstrings | 18 +++++++++++++++--- Meshtastic/Enums/AppSettingsEnums.swift | 1 - Meshtastic/Views/Nodes/MeshMap.swift | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a61ea1cf..42554c57 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -21,6 +21,21 @@ }, ": %d" : { + }, + ".dot" : { + + }, + ".gauge" : { + + }, + ".gradient" : { + + }, + ".pill" : { + + }, + ".text" : { + }, "(Re)define PIN_GPS_EN for your board." : { @@ -19154,9 +19169,6 @@ }, "Send" : { - }, - "Send ${messageContent} to ${channelNumber}" : { - }, "Send a Group Message" : { diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index bfd8ce9e..690fe399 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -59,7 +59,6 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable { case fifteenHundredMiles = 2414016 case twentyFiveHundredMiles = 4023360 case fiveThouandMiles = 8046720 - case tenThousandMiles = 16093440 var id: Double { self.rawValue } var description: String { let distanceFormatter = MKDistanceFormatter() diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index c4f987ab..5dd6e19a 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -205,7 +205,7 @@ struct MeshMap: View { .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) - .onAppear { + .onFirstAppear { UIApplication.shared.isIdleTimerDisabled = true // let wayPointEntity = getWaypoint(id: Int64(deepLinkManager.waypointId) ?? -1, context: context) From 7d94dc4264b580512287b220b21675a60b7ea306 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 07:07:25 -0700 Subject: [PATCH 220/333] Clean up local stats code --- Meshtastic/Enums/TelemetryEnums.swift | 27 ++++++++++++++++++++++++ Meshtastic/Helpers/MeshPackets.swift | 13 +++++------- Meshtastic/Views/Bluetooth/Connect.swift | 8 ++++--- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Enums/TelemetryEnums.swift b/Meshtastic/Enums/TelemetryEnums.swift index 187a8d74..8de53403 100644 --- a/Meshtastic/Enums/TelemetryEnums.swift +++ b/Meshtastic/Enums/TelemetryEnums.swift @@ -176,3 +176,30 @@ enum Iaq: Int, CaseIterable, Identifiable { return iaq } } + + +// Default of 0 is Client +enum MetricsTypes: Int, CaseIterable, Identifiable { + + case device = 0 + case environment = 1 + case power = 2 + case airQuality = 3 + case stats = 4 + + var id: Int { self.rawValue } + var name: String { + switch self { + case .device: + return "Device Metrics" + case .environment: + return "Environment Metrics" + case .power: + return "Power Metrics" + case .airQuality: + return "Air Quality Metrics" + case .stats: + return "Stats" + } + } +} diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 09683c0e..ca33212e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -675,7 +675,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { - if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { + if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) MeshLogger.log("📈 \(logString)") @@ -729,7 +729,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) - telemetry.metricsType = 6 + telemetry.metricsType = 4 Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)") } telemetry.snr = packet.rxSnr @@ -748,7 +748,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage } try context.save() - Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())") + Logger.data.info("💾 [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type") Saved for Node: \(packet.from.toHex())") if telemetry.metricsType == 0 { // Connected Device Metrics // ------------------------ @@ -769,9 +769,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage manager.schedule() } } - } else if telemetry.metricsType == 6 { + } else if telemetry.metricsType == 4 { // Update our live activity if there is one running, not available on mac iOS >= 16.2 -#if !targetEnvironment(macCatalyst) +#if canImport(ActivityKit) let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! let date = Date.now...fifteenMinutesLater @@ -843,13 +843,10 @@ func textMessageAppPacket( if messageText?.count ?? 0 > 0 { MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") - let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) - do { let fetchedUsers = try context.fetch(messageUsers) - let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) if packet.rxTime > 0 { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 3fdab7fd..911bf10e 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,6 +27,7 @@ struct Connect: View { @State var invalidFirmwareVersion = false @State var liveActivityStarted = false @State var selectedPeripherialId = "" + @State var localStats: TelemetryEntity? init () { let notificationCenter = UNUserNotificationCenter.current() @@ -91,8 +92,6 @@ struct Connect: View { } } VStack { - let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity - if localStats != nil { Divider() if localStats?.numTotalNodes ?? 0 >= 100 { @@ -351,6 +350,9 @@ struct Connect: View { .onChange(of: (self.bleManager.invalidVersion)) { _ in invalidFirmwareVersion = self.bleManager.invalidVersion } + .onChange(of: [node?.telemetries]) { _ in + localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4")).lastObject as? TelemetryEntity + } .onChange(of: (self.bleManager.isSubscribed)) { sub in if UserDefaults.preferredPeripheralId.count > 0 && sub { @@ -376,7 +378,7 @@ struct Connect: View { liveActivityStarted = true // 15 Minutes Local Stats Interval let timerSeconds = 900 - let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")) + let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4")) let mostRecent = localStats?.lastObject as? TelemetryEntity let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown") From f3a23bb3110c4a1341eb7a8383817fde0bf31036 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 07:12:53 -0700 Subject: [PATCH 221/333] Take temp font down a couple of sizes --- Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 3ea1dee3..e4867544 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -106,7 +106,7 @@ struct WeatherConditionsCompactWidget: View { .font(.caption) } Text(temperature) - .font(temperature.length < 4 ? .system(size: 76) : .system(size: 60) ) + .font(temperature.length < 4 ? .system(size: 72) : .system(size: 54) ) } .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) .padding() From 09bd3059d7c7502ad521a3f1dee26d6e3df9f86d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 07:49:55 -0700 Subject: [PATCH 222/333] Imporoved length validation --- Meshtastic/Extensions/String.swift | 1 - .../Views/Messages/TextMessageField/TextMessageField.swift | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6255b151..4e840a98 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -91,5 +91,4 @@ extension String { let end = index(start, offsetBy: range.upperBound - range.lowerBound) return String(self[start ..< end]) } - } diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index 12b72ae9..7aded0f1 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -33,8 +33,9 @@ struct TextMessageField: View { .onChange(of: typingMessage, perform: { value in totalBytes = value.utf8.count // Only mess with the value if it is too big - if totalBytes > Self.maxbytes { + while totalBytes > Self.maxbytes { typingMessage = String(typingMessage.dropLast()) + totalBytes = typingMessage.utf8.count } }) .keyboardType(.default) From 25143ddf755487e2516937769c5c2951d1330703 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 08:03:18 -0700 Subject: [PATCH 223/333] while --- .../Views/MapKitMap/WaypointFormMapKit.swift | 11 ++++++--- .../Nodes/Helpers/Map/WaypointForm.swift | 10 ++++---- .../Views/Settings/Channels/ChannelForm.swift | 5 ++-- .../Views/Settings/Config/DeviceConfig.swift | 6 ++--- .../Config/Module/CannedMessagesConfig.swift | 5 ++-- .../Config/Module/DetectionSensorConfig.swift | 6 ++--- .../Settings/Config/Module/MQTTConfig.swift | 23 ++++++++++--------- .../Settings/Config/Module/RtttlConfig.swift | 6 ++--- .../Views/Settings/Config/NetworkConfig.swift | 10 ++++---- Meshtastic/Views/Settings/Routes.swift | 6 ++--- Meshtastic/Views/Settings/UserConfig.swift | 8 ++++--- 11 files changed, 55 insertions(+), 41 deletions(-) diff --git a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index d5fd9c01..5fc5e2f4 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -52,8 +52,12 @@ struct WaypointFormMapKit: View { ) .foregroundColor(Color.gray) .onChange(of: name, perform: { _ in - let totalBytes = name.utf8.count + var totalBytes = name.utf8.count // Only mess with the value if it is too big + while totalBytes > 30 { + name = String(name.dropLast()) + totalBytes = name.utf8.count + } if totalBytes > 30 { name = String(name.dropLast()) } @@ -69,10 +73,11 @@ struct WaypointFormMapKit: View { ) .foregroundColor(Color.gray) .onChange(of: description, perform: { _ in - let totalBytes = description.utf8.count + var totalBytes = description.utf8.count // Only mess with the value if it is too big - if totalBytes > 100 { + while totalBytes > 100 { description = String(description.dropLast()) + totalBytes = description.utf8.count } }) } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 35916eb6..4261b0cc 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -66,10 +66,11 @@ struct WaypointForm: View { ) .foregroundColor(Color.gray) .onChange(of: name, perform: { _ in - let totalBytes = name.utf8.count + var totalBytes = name.utf8.count // Only mess with the value if it is too big - if totalBytes > 30 { + while totalBytes > 30 { name = String(name.dropLast()) + totalBytes = name.utf8.count } }) } @@ -83,10 +84,11 @@ struct WaypointForm: View { ) .foregroundColor(Color.gray) .onChange(of: description, perform: { _ in - let totalBytes = description.utf8.count + var totalBytes = description.utf8.count // Only mess with the value if it is too big - if totalBytes > 100 { + while totalBytes > 100 { description = String(description.dropLast()) + totalBytes = description.utf8.count } }) } diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index e4930b7a..72e17cef 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -43,10 +43,11 @@ struct ChannelForm: View { .foregroundColor(Color.gray) .onChange(of: channelName, perform: { _ in channelName = channelName.replacing(" ", with: "") - let totalBytes = channelName.utf8.count + var totalBytes = channelName.utf8.count // Only mess with the value if it is too big - if totalBytes > 11 { + while totalBytes > 11 { channelName = String(channelName.dropLast()) + totalBytes = channelName.utf8.count } hasChanges = true }) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index c78cb845..f9efd066 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -94,14 +94,14 @@ struct DeviceConfig: View { TextField("Time Zone", text: $tzdef, axis: .vertical) .foregroundColor(.gray) .onChange(of: tzdef, perform: { _ in - let totalBytes = tzdef.utf8.count + var totalBytes = tzdef.utf8.count // Only mess with the value if it is too big - if totalBytes > 63 { + while totalBytes > 63 { tzdef = String(tzdef.dropLast()) + totalBytes = tzdef.utf8.count } }) .foregroundColor(.gray) - } .keyboardType(.default) .disableAutocorrection(true) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 5b94e8c3..568be9da 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -73,10 +73,11 @@ struct CannedMessagesConfig: View { .disableAutocorrection(true) .onChange(of: messages, perform: { _ in - let totalBytes = messages.utf8.count + var totalBytes = messages.utf8.count // Only mess with the value if it is too big - if totalBytes > 198 { + while totalBytes > 198 { messages = String(messages.dropLast()) + totalBytes = messages.utf8.count } hasMessagesChanges = true }) diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index e67898eb..6305dd98 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -92,11 +92,11 @@ struct DetectionSensorConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: name, perform: { _ in - - let totalBytes = name.utf8.count + var totalBytes = name.utf8.count // Only mess with the value if it is too big - if totalBytes > 20 { + while totalBytes > 20 { name = String(name.dropLast()) + totalBytes = name.utf8.count } }) } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 1996c744..6b39b649 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -124,10 +124,11 @@ struct MQTTConfig: View { TextField("Root Topic", text: $root) .foregroundColor(.gray) .onChange(of: root, perform: { _ in - let totalBytes = root.utf8.count + var totalBytes = root.utf8.count // Only mess with the value if it is too big - if totalBytes > 30 { + while totalBytes > 30 { root = String(root.dropLast()) + totalBytes = root.utf8.count } }) .foregroundColor(.gray) @@ -162,10 +163,11 @@ struct MQTTConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: address, perform: { _ in - let totalBytes = address.utf8.count + var totalBytes = address.utf8.count // Only mess with the value if it is too big - if totalBytes > 62 { + while totalBytes > 62 { address = String(address.dropLast()) + totalBytes = address.utf8.count } hasChanges = true }) @@ -180,12 +182,11 @@ struct MQTTConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: username, perform: { _ in - - let totalBytes = username.utf8.count - + var totalBytes = username.utf8.count // Only mess with the value if it is too big - if totalBytes > 62 { + while totalBytes > 62 { username = String(username.dropLast()) + totalBytes = username.utf8.count } hasChanges = true }) @@ -200,11 +201,11 @@ struct MQTTConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: password, perform: { _ in - - let totalBytes = password.utf8.count + var totalBytes = password.utf8.count // Only mess with the value if it is too big - if totalBytes > 62 { + while totalBytes > 62 { password = String(password.dropLast()) + totalBytes = password.utf8.count } hasChanges = true }) diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 46b02848..772b6b98 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -32,11 +32,11 @@ struct RtttlConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: ringtone, perform: { _ in - - let totalBytes = ringtone.utf8.count + var totalBytes = ringtone.utf8.count // Only mess with the value if it is too big - if totalBytes > 228 { + while totalBytes > 228 { ringtone = String(ringtone.dropLast()) + totalBytes = ringtone.utf8.count } }) .foregroundColor(.gray) diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 928bad57..198ad1a0 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -46,10 +46,11 @@ struct NetworkConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: wifiSsid, perform: { _ in - let totalBytes = wifiSsid.utf8.count + var totalBytes = wifiSsid.utf8.count // Only mess with the value if it is too big - if totalBytes > 32 { + while totalBytes > 32 { wifiSsid = String(wifiSsid.dropLast()) + totalBytes = wifiSsid.utf8.count } hasChanges = true }) @@ -63,10 +64,11 @@ struct NetworkConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: wifiPsk, perform: { _ in - let totalBytes = wifiPsk.utf8.count + var totalBytes = wifiPsk.utf8.count // Only mess with the value if it is too big - if totalBytes > 63 { + while totalBytes > 63 { wifiPsk = String(wifiPsk.dropLast()) + totalBytes = wifiPsk.utf8.count } hasChanges = true }) diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 65be4fd3..314497e8 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -177,11 +177,11 @@ struct Routes: View { ) .foregroundColor(Color.gray) .onChange(of: name, perform: { _ in - let totalBytes = name.utf8.count + var totalBytes = name.utf8.count // Only mess with the value if it is too big - - if totalBytes > 100 { + while totalBytes > 100 { name = String(name.dropLast()) + totalBytes = name.utf8.count } }) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index c67c70bf..72084954 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -50,10 +50,11 @@ struct UserConfig: View { TextField("Long Name", text: $longName) .onChange(of: longName, perform: { _ in - let totalBytes = longName.utf8.count + var totalBytes = longName.utf8.count // Only mess with the value if it is too big - if totalBytes > (isLicensed ? 6 : 36) { + while totalBytes > (isLicensed ? 6 : 36) { longName = String(longName.dropLast()) + totalBytes = longName.utf8.count } }) } @@ -74,10 +75,11 @@ struct UserConfig: View { TextField("Short Name", text: $shortName) .foregroundColor(.gray) .onChange(of: shortName, perform: { _ in - let totalBytes = shortName.utf8.count + var totalBytes = shortName.utf8.count // Only mess with the value if it is too big if totalBytes > 4 { shortName = String(shortName.dropLast()) + totalBytes = shortName.utf8.count } }) .foregroundColor(.gray) From 2f7b087ff2ee9c5f20484fd46ac0054b26be60ec Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 08:20:51 -0700 Subject: [PATCH 224/333] Remove local stats from bluetooth connect view as they don't update --- Localizable.xcstrings | 33 ------------------ Meshtastic/Views/Bluetooth/Connect.swift | 43 ------------------------ 2 files changed, 76 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0ea54454..9650f2ca 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -140,16 +140,6 @@ }, "%@%%" : { - }, - "%@%% %@%%" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@%% %2$@%%" - } - } - } }, "%@°F" : { @@ -1428,16 +1418,6 @@ }, "Bad" : { - }, - "Bad Packets: %d %@%%" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Bad Packets: %1$d %2$@%%" - } - } - } }, "Bandwidth" : { @@ -16085,16 +16065,6 @@ }, "Override automatic OLED screen detection." : { - }, - "Packets: Sent: %d Received: %d" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Packets: Sent: %1$d Received: %2$d" - } - } - } }, "password" : { "localizations" : { @@ -22325,9 +22295,6 @@ } } } - }, - "Uptime: %@" : { - }, "Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead." : { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 911bf10e..f7aaa738 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,7 +27,6 @@ struct Connect: View { @State var invalidFirmwareVersion = false @State var liveActivityStarted = false @State var selectedPeripherialId = "" - @State var localStats: TelemetryEntity? init () { let notificationCenter = UNUserNotificationCenter.current() @@ -91,45 +90,6 @@ struct Connect: View { } } } - VStack { - if localStats != nil { - Divider() - if localStats?.numTotalNodes ?? 0 >= 100 { - Text("\(String(format: "Connected: %d nodes online", localStats?.numOnlineNodes ?? 0))") - .font(.callout) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .fixedSize() - } else { - Text("\(String(format: "Connected: %d of %d nodes online", localStats?.numOnlineNodes ?? 0, localStats?.numTotalNodes ?? 0))") - .font(.callout) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .fixedSize() - } - Text("\(String(format: "Ch. Util: %.2f", localStats?.channelUtilization ?? 0))% \(String(format: "Airtime: %.2f", localStats?.airUtilTx ?? 0))%") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - Text("Packets: Sent: \(localStats?.numPacketsTx ?? 0) Received: \(localStats?.numPacketsRx ?? 0)") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - let errorRate = (Double(localStats?.numPacketsRxBad ?? -1) / Double(localStats?.numPacketsRx ?? -1)) * 100 - Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0) \(String(format: "Error Rate: %.2f", errorRate))%") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .fixedSize() - let now = Date.now - let later = now + TimeInterval(Double(localStats?.numPacketsRxBad ?? 0)) - let uptime = (now.. 0 && sub { From 3ff173d0a02ebd5013c89473968e7ce5b1af0dea Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 23:31:26 -0700 Subject: [PATCH 225/333] Trace route mockup --- Localizable.xcstrings | 3 + Meshtastic.xcodeproj/project.pbxproj | 16 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 3 + .../contents | 476 ++++++++++++++++++ Meshtastic/Views/Layouts/TraceRoute.swift | 68 +++ Meshtastic/Views/Messages/MessageText.swift | 2 +- Meshtastic/Views/Nodes/TraceRouteLog.swift | 68 ++- 8 files changed, 627 insertions(+), 11 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents create mode 100644 Meshtastic/Views/Layouts/TraceRoute.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9650f2ca..3062e336 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15,6 +15,9 @@ }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { + }, + "-12dB" : { + }, ": %@" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 353226be..c8a3006f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D5A322CA1178300ED3032 /* TraceRoute.swift */; }; DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65752C6EA5490053C113 /* AckErrors.swift */; }; @@ -365,6 +366,8 @@ DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRoute.swift; sourceTree = ""; }; + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 45.xcdatamodel"; sourceTree = ""; }; DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = ""; }; @@ -747,6 +750,14 @@ path = Module; sourceTree = ""; }; + DD6D5A312CA1176A00ED3032 /* Layouts */ = { + isa = PBXGroup; + children = ( + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */, + ); + path = Layouts; + sourceTree = ""; + }; DD6F65772C6EAB860053C113 /* Help */ = { isa = PBXGroup; children = ( @@ -901,6 +912,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + DD6D5A312CA1176A00ED3032 /* Layouts */, C9483F6B2773016700998F6B /* MapKitMap */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, @@ -1433,6 +1445,7 @@ DD6F65742C6CB80A0053C113 /* View.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, @@ -1899,6 +1912,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */, DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, @@ -1944,7 +1958,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */; + currentVersion = DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 8a88003b..4269da21 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 44.xcdatamodel + MeshtasticDataModelV 45.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents index 98de0347..ae7c08e4 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents @@ -428,11 +428,14 @@ + + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents new file mode 100644 index 00000000..98de0347 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift new file mode 100644 index 00000000..bb79f747 --- /dev/null +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -0,0 +1,68 @@ +// +// TraceRoute.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/22/24. +// +import SwiftUI + +struct Rotation: LayoutValueKey { + static let defaultValue: Binding? = nil +} + +struct TraceRouteComponent: View { + var animation: Animation? + @ViewBuilder let content: () -> V + @State private var rotation: Angle = .zero + + var body: some View { + content() + .rotationEffect(rotation) + .layoutValue(key: Rotation.self, value: $rotation.animation(animation)) + } +} + +struct TraceRoute: Layout { + var animatableData: AnimatablePair { + get { + AnimatablePair(rotation.radians, radius) + } + set { + rotation = Angle.radians(newValue.first) + radius = newValue.second + } + } + + var radius: CGFloat + var rotation: Angle + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) { + return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) + } + return CGSize(width: (maxSize.width / 2 + radius) * 2, + height: (maxSize.height / 2 + radius) * 2) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let angleStep = (Angle.degrees(360).radians / Double(subviews.count)) + + for (index, subview) in subviews.enumerated() { + let angle = angleStep * CGFloat(index) + rotation.radians + + var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle)) + point.x += bounds.midX + point.y += bounds.midY + + subview.place(at: point, anchor: .center, proposal: .unspecified) + + DispatchQueue.main.async { + if index % 2 == 0 { + subview[Rotation.self]?.wrappedValue = .zero + } else { + subview[Rotation.self]?.wrappedValue = .radians(angle) + } + } + } + } +} diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 1f2cc017..25347c25 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -30,7 +30,7 @@ struct MessageText: View { .background(isCurrentUser ? .accentColor : Color(.gray)) .cornerRadius(15) .overlay { - if message.pkiEncrypted && message.ackError == 0 && message.realACK { + if message.pkiEncrypted && message.ackError <= 0 && (message.realACK || message.ackError == nil) { VStack(alignment: .trailing) { Spacer() HStack { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 345a4299..0f947ecf 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -15,7 +15,6 @@ struct TraceRouteLog: View { @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" @@ -26,13 +25,21 @@ struct TraceRouteLog: View { @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic let distanceFormatter = MKDistanceFormatter() + /// Mockup Values + let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green] + let nums: [Int64] = [366311664, 0, 3662955168, 0, 3663982804, 0, 4202719792, 0, 603700594, 0, 836212501, 0, 3663116644, 0, 8362955168] + let snr: [Double] = [-115.00, 17.5, 7.0, 8.9, -24.0, 5.5, 6.0, 7.5] + @State private var hops: Int = 16 /// Max of 16 (2 8 hop routes) + /// State for the circle of routes + @State var angle: Angle = .zero + @State var radius: CGFloat = 230.00 + @State var animation: Animation? var body: some View { HStack(alignment: .top) { VStack { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in - Label { Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")") } icon: { @@ -45,8 +52,33 @@ struct TraceRouteLog: View { .frame(minHeight: 200, maxHeight: 230) VStack { if selectedRoute != nil { + if true {// selectedRoute?.hops?.count ?? 2 > 3 { + VStack { + Spacer() + HStack(spacing: 15) { + TraceRoute(radius: radius, rotation: angle) { + contents() + } + } + .onAppear { + // Set the view rotation animation after the view appeared, + // to avoid animating initial rotation + DispatchQueue.main.async { + animation = .easeInOut(duration: 1.0) + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) + } + } + } + .onTapGesture { + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { - Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { @@ -94,8 +126,6 @@ struct TraceRouteLog: View { MapPolyline(coordinates: traceRouteCoords) .stroke(.blue, style: dashed) } - } else if selectedRoute?.hops?.count ?? 0 == 0 { - } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -104,10 +134,10 @@ struct TraceRouteLog: View { /// Distance if selectedRoute?.node?.positions?.count ?? 0 > 0, selectedRoute?.coordinate != nil, - let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - + let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { + let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) - + if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) Label { @@ -145,4 +175,26 @@ struct TraceRouteLog: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) } + @ViewBuilder func contents(animation: Animation? = nil) -> some View { + ForEach(0.. 0 { + Text("-12dB") + .font(.caption) + .foregroundColor(colors[idx%colors.count].opacity(0.7)) + } + } + } else { + Image(systemName: "arrowshape.right.fill") + .resizable() + .frame(width: 35, height: 35) + .foregroundColor(colors[idx%colors.count].opacity(0.7)) + } + } + } + } } From 8876babd55ece1d7c6015b4dd7ac7b39aacaf274 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Sep 2024 23:32:11 -0700 Subject: [PATCH 226/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c8a3006f..35461588 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1700,7 +1700,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1735,7 +1735,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1767,7 +1767,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1800,7 +1800,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 1de64e6659167b7bc26275bbfb465703b18af9f7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Sep 2024 10:46:38 -0700 Subject: [PATCH 227/333] Move wheel of traceroutes into details --- Localizable.xcstrings | 18 ++----- Meshtastic/Views/Nodes/TraceRouteLog.swift | 56 +++++++++++----------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3062e336..391fdce4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -24,21 +24,6 @@ }, ": %d" : { - }, - ".dot" : { - - }, - ".gauge" : { - - }, - ".gradient" : { - - }, - ".pill" : { - - }, - ".text" : { - }, "(Re)define PIN_GPS_EN for your board." : { @@ -19142,6 +19127,9 @@ }, "Send" : { + }, + "Send ${messageContent} to ${channelNumber}" : { + }, "Send a Group Message" : { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 0f947ecf..9498f3e6 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -32,7 +32,7 @@ struct TraceRouteLog: View { @State private var hops: Int = 16 /// Max of 16 (2 8 hop routes) /// State for the circle of routes @State var angle: Angle = .zero - @State var radius: CGFloat = 230.00 + @State var radius: CGFloat = 175.00 @State var animation: Animation? var body: some View { @@ -52,32 +52,6 @@ struct TraceRouteLog: View { .frame(minHeight: 200, maxHeight: 230) VStack { if selectedRoute != nil { - if true {// selectedRoute?.hops?.count ?? 2 > 3 { - VStack { - Spacer() - HStack(spacing: 15) { - TraceRoute(radius: radius, rotation: angle) { - contents() - } - } - .onAppear { - // Set the view rotation animation after the view appeared, - // to avoid animating initial rotation - DispatchQueue.main.async { - animation = .easeInOut(duration: 1.0) - withAnimation(.easeInOut(duration: 2.0)) { - angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) - } - } - } - .onTapGesture { - withAnimation(.easeInOut(duration: 2.0)) { - angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") @@ -135,9 +109,7 @@ struct TraceRouteLog: View { if selectedRoute?.node?.positions?.count ?? 0 > 0, selectedRoute?.coordinate != nil, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) - if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) Label { @@ -162,6 +134,32 @@ struct TraceRouteLog: View { Spacer() } } + if true {// selectedRoute?.hops?.count ?? 2 > 3 { + VStack { + Spacer() + HStack(spacing: 15) { + TraceRoute(radius: radius, rotation: angle) { + contents() + } + } + .onAppear { + // Set the view rotation animation after the view appeared, + // to avoid animating initial rotation + DispatchQueue.main.async { + animation = .easeInOut(duration: 1.0) + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) + } + } + } + .onTapGesture { + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } } else { ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } From 75c90e35e7c54d8601c7a9e35c11323d7bcc7d4d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Sep 2024 11:07:50 -0700 Subject: [PATCH 228/333] Show lock for incoming encrypted messages --- Localizable.xcstrings | 18 +++--------------- Meshtastic/Views/Messages/MessageText.swift | 3 ++- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9650f2ca..58f780d0 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -21,21 +21,6 @@ }, ": %d" : { - }, - ".dot" : { - - }, - ".gauge" : { - - }, - ".gradient" : { - - }, - ".pill" : { - - }, - ".text" : { - }, "(Re)define PIN_GPS_EN for your board." : { @@ -19139,6 +19124,9 @@ }, "Send" : { + }, + "Send ${messageContent} to ${channelNumber}" : { + }, "Send a Group Message" : { diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 1f2cc017..411511fd 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -30,7 +30,8 @@ struct MessageText: View { .background(isCurrentUser ? .accentColor : Color(.gray)) .cornerRadius(15) .overlay { - if message.pkiEncrypted && message.ackError == 0 && message.realACK { + /// Show the lock if the message is pki encrypted and has a real ack if sent by the current user, or is pki encrypted for incoming messages + if message.pkiEncrypted && message.realACK || !isCurrentUser && message.pkiEncrypted { VStack(alignment: .trailing) { Spacer() HStack { From 22c27ce27c6d9a9f282beadc2571d9197be81433 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Sep 2024 11:40:56 -0700 Subject: [PATCH 229/333] Bump version, remove unnessary myinfo query --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Helpers/BLEManager.swift | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 353226be..d13cc990 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1687,7 +1687,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1722,7 +1722,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1754,7 +1754,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1787,7 +1787,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.6; + MARKETING_VERSION = 2.5.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index aae1a8ab..fc3b4dcf 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -680,9 +680,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { disconnectPeripheral(reconnect: false) try container.restorePersistentStore(from: databasePath) - context.refreshAllObjects() - let request = MyInfoEntity.fetchRequest() - try context.fetch(request) UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0) connectTo(peripheral: peripheral) Logger.data.notice("🗂️ Restored Core data for /\(UserDefaults.preferredPeripheralNum, privacy: .public)") From 60873ec647e29d799b4bf41995c25ea4fd937066 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Sep 2024 12:58:22 -0700 Subject: [PATCH 230/333] Logging for client notification --- Meshtastic/Helpers/BLEManager.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index fc3b4dcf..e62093e9 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -652,6 +652,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate retained: decodedInfo.mqttClientProxyMessage.retained ) mqttManager.mqttClientProxy?.publish(message) + } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { + Logger.data.error("⚠️ Client Notification") } switch decodedInfo.packet.decoded.portnum { @@ -942,7 +944,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Use a RunLoop to prevent the timer from running on the main UI thread if UserDefaults.provideLocation { let interval = UserDefaults.provideLocationInterval >= 10 ? UserDefaults.provideLocationInterval : 30 - positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(3.0), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(3.0), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } From 506772bf619940f222315133ef8f4d1e9fa469c4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Sep 2024 16:38:18 -0700 Subject: [PATCH 231/333] Client notifications --- Meshtastic/Helpers/BLEManager.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index e62093e9..894401c7 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -653,7 +653,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { - Logger.data.error("⚠️ Client Notification") + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: UUID().uuidString, + title: "Firmware Notification", + subtitle: "\(decodedInfo.clientNotification.level)".capitalized, + content: decodedInfo.clientNotification.message, + target: "settings", + path: "meshtastic:///settings/debugLogs" + ) + ] + manager.schedule() + Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure")") } switch decodedInfo.packet.decoded.portnum { @@ -944,8 +956,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Use a RunLoop to prevent the timer from running on the main UI thread if UserDefaults.provideLocation { let interval = UserDefaults.provideLocationInterval >= 10 ? UserDefaults.provideLocationInterval : 30 - positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(3.0), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) - positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(3.0), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } From 78903f442ab43bfed5f0085291fc5a5ef9123a7c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 24 Sep 2024 16:04:14 -0700 Subject: [PATCH 232/333] Wheel of traceroute --- Localizable.xcstrings | 14 +- Meshtastic/Helpers/BLEManager.swift | 36 ++- .../contents | 4 +- Meshtastic/Views/Nodes/TraceRouteLog.swift | 212 +++++++++--------- 4 files changed, 150 insertions(+), 116 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 391fdce4..95809869 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15,9 +15,6 @@ }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { - }, - "-12dB" : { - }, ": %@" : { @@ -21859,8 +21856,15 @@ "Trace Route Log" : { }, - "Trace route received directly by %@" : { - + "Trace route received directly by %@ with a SNR of %@ dB" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Trace route received directly by %1$@ with a SNR of %2$@ dB" + } + } + } }, "Trace Route Sent" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index aae1a8ab..63cf1883 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -825,7 +825,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: - if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + if let routingMessage = try? RouteDiscovery(serializedBytes: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true traceRoute?.route = routingMessage.route @@ -836,13 +836,45 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] - for node in routingMessage.route { + for (index, node) in routingMessage.route.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() + traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) + if hopNode?.hasPositions ?? false { + traceRoute?.hasPositions = true + if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + traceRouteHop.altitude = mostRecent.altitude + traceRouteHop.latitudeI = mostRecent.latitudeI + traceRouteHop.longitudeI = mostRecent.longitudeI + traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized + } else { + traceRoute?.hasPositions = false + } + } else { + traceRoute?.hasPositions = false + } + traceRouteHop.num = hopNode?.num ?? 0 + if hopNode != nil { + if decodedInfo.packet.rxTime > 0 { + hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) + } + hopNodes.append(traceRouteHop) + } + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") \(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB --> " + } + for (index, node) in routingMessage.routeBack.enumerated() { + var hopNode = getNodeInfo(id: Int64(node), context: context) + if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { + hopNode = createNodeInfo(num: Int64(node), context: context) + } + let traceRouteHop = TraceRouteHopEntity(context: context) + traceRouteHop.time = Date() + traceRouteHop.back = true + traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) if hopNode?.hasPositions ?? false { traceRoute?.hasPositions = true if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index 98de0347..265c23f8 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -428,10 +428,12 @@ + + diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 9498f3e6..87e101f1 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -12,6 +12,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct TraceRouteLog: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -25,14 +26,9 @@ struct TraceRouteLog: View { @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic let distanceFormatter = MKDistanceFormatter() - /// Mockup Values - let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green] - let nums: [Int64] = [366311664, 0, 3662955168, 0, 3663982804, 0, 4202719792, 0, 603700594, 0, 836212501, 0, 3663116644, 0, 8362955168] - let snr: [Double] = [-115.00, 17.5, 7.0, 8.9, -24.0, 5.5, 6.0, 7.5] - @State private var hops: Int = 16 /// Max of 16 (2 8 hop routes) + let modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast /// State for the circle of routes @State var angle: Angle = .zero - @State var radius: CGFloat = 175.00 @State var animation: Animation? var body: some View { @@ -49,8 +45,9 @@ struct TraceRouteLog: View { } .listStyle(.plain) } - .frame(minHeight: 200, maxHeight: 230) - VStack { + .frame(minHeight: CGFloat(node.traceRoutes?.count ?? 0 * 40), maxHeight: 150) + Divider() + ScrollView { if selectedRoute != nil { if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { Label { @@ -59,112 +56,115 @@ struct TraceRouteLog: View { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) } - .font(.title2) + .font(.title3) } else if selectedRoute?.response ?? false { Label { - Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") } icon: { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) } - .font(.title2) - } - if selectedRoute?.response ?? false { - if selectedRoute?.hasPositions ?? false { - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - .annotationTitles(.automatic) - // Direct Trace Route - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { - if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] - Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.black)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - let dashed = StrokeStyle( - lineWidth: 2, - lineCap: .round, lineJoin: .round, dash: [7, 10] - ) - MapPolyline(coordinates: traceRouteCoords) - .stroke(.blue, style: dashed) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - VStack { - /// Distance - if selectedRoute?.node?.positions?.count ?? 0 > 0, - selectedRoute?.coordinate != nil, - let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) - if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) - Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") - .foregroundColor(.primary) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - } - } - } - } + .font(.title3) } else { VStack { - Label { - Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - } - .font(.title3) - Spacer() + Label { + Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + .font(idiom == .phone ? .headline : .largeTitle) } } - if true {// selectedRoute?.hops?.count ?? 2 > 3 { - VStack { - Spacer() - HStack(spacing: 15) { - TraceRoute(radius: radius, rotation: angle) { + if selectedRoute?.hops?.count ?? 2 > 3 { + HStack(alignment: .center) { + GeometryReader { geometry in + let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 50 : 70) + Spacer() + TraceRoute(radius: size, rotation: angle) { contents() } + .padding(.leading) } - .onAppear { - // Set the view rotation animation after the view appeared, - // to avoid animating initial rotation - DispatchQueue.main.async { - animation = .easeInOut(duration: 1.0) - withAnimation(.easeInOut(duration: 2.0)) { - angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) + .scaledToFit() + } + .onAppear { + // Set the view rotation animation after the view appeared, + // to avoid animating initial rotation + DispatchQueue.main.async { + animation = .easeInOut(duration: 1.0) + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) + } + } + } + .onTapGesture { + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) + } + } + } + if selectedRoute?.hasPositions ?? false { + Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { + Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.green)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + // Direct Trace Route + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { + if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { + let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] + Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + let dashed = StrokeStyle( + lineWidth: 2, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: traceRouteCoords) + .stroke(.blue, style: dashed) + } + } + } + .frame(maxWidth: .infinity, minHeight: 250) + if selectedRoute?.response ?? false { + VStack { + /// Distance + if selectedRoute?.node?.positions?.count ?? 0 > 0, + selectedRoute?.coordinate != nil, + let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { + let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) + if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") + .foregroundColor(.primary) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + } } } } - .onTapGesture { - withAnimation(.easeInOut(duration: 2.0)) { - angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) - } - } } - .frame(maxWidth: .infinity, maxHeight: .infinity) + Spacer() + .padding(.bottom, 125) } } else { ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } } - Spacer() + .edgesIgnoringSafeArea(.bottom) } .navigationTitle("Trace Route Log") } @@ -174,24 +174,20 @@ struct TraceRouteLog: View { }) } @ViewBuilder func contents(animation: Animation? = nil) -> some View { - ForEach(0.. 0 { - Text("-12dB") - .font(.caption) - .foregroundColor(colors[idx%colors.count].opacity(0.7)) - } - } - } else { - Image(systemName: "arrowshape.right.fill") - .resizable() - .frame(width: 35, height: 35) - .foregroundColor(colors[idx%colors.count].opacity(0.7)) + let nodeColor = UIColor(hex: UInt32(truncatingIfNeeded: idx.num)) + let snrColor = getSnrColor(snr: idx.snr, preset: modemPreset) + VStack { + CircleText(text: String(idx.num.toHex().suffix(4)), color: Color(nodeColor), circleSize: idiom == .phone ? 70 : 100) + Text("\(String(format: "%.2f", idx.snr)) dB") + .font(idiom == .phone ? .caption : .headline) + .foregroundColor(snrColor) } + Image(systemName: "arrowshape.right.fill") + .resizable() + .frame(width: idiom == .phone ? 25 : 40, height: idiom == .phone ? 25 : 40) + .foregroundColor(snrColor.opacity(0.7)) } } } From 5e992146563c0dc7e426d9a7486d70f412b8ea1a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 24 Sep 2024 16:06:19 -0700 Subject: [PATCH 233/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 35461588..d9146f08 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1700,7 +1700,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1735,7 +1735,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1767,7 +1767,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1800,7 +1800,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From c400a2273a810a05d3bbece96284e3c09f9dd617 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 24 Sep 2024 16:15:42 -0700 Subject: [PATCH 234/333] Leave wheel of traceroute mockup --- Localizable.xcstrings | 3 +++ Meshtastic/Views/Nodes/TraceRouteLog.swift | 29 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 95809869..78871f94 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15,6 +15,9 @@ }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { + }, + "-12dB" : { + }, ": %@" : { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 87e101f1..0b385205 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -26,9 +26,16 @@ struct TraceRouteLog: View { @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic let distanceFormatter = MKDistanceFormatter() - let modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast + var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast + + /// Mockup Values + let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green] + let nums: [Int64] = [366311664, 0, 3662955168, 0, 3663982804, 0, 4202719792, 0, 603700594, 0, 836212501, 0, 3663116644, 0, 8362955168] + let snr: [Double] = [-115.00, 17.5, 7.0, 8.9, -24.0, 5.5, 6.0, 7.5] + @State private var hops: Int = 16 /// Max of 16 (2 8 hop routes) /// State for the circle of routes @State var angle: Angle = .zero + //@State var radius: CGFloat = 175.00 @State var animation: Animation? var body: some View { @@ -76,7 +83,7 @@ struct TraceRouteLog: View { .font(idiom == .phone ? .headline : .largeTitle) } } - if selectedRoute?.hops?.count ?? 2 > 3 { + if true { // selectedRoute?.hops?.count ?? 2 > 3 { HStack(alignment: .center) { GeometryReader { geometry in let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 50 : 70) @@ -174,6 +181,24 @@ struct TraceRouteLog: View { }) } @ViewBuilder func contents(animation: Animation? = nil) -> some View { + ForEach(0.. Date: Tue, 24 Sep 2024 17:17:42 -0700 Subject: [PATCH 235/333] Hook up the wheel of traceroutes --- Localizable.xcstrings | 3 -- Meshtastic/Resources/DeviceHardware.json | 16 ++++++++ Meshtastic/Views/Nodes/TraceRouteLog.swift | 46 +++++++--------------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 78871f94..95809869 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15,9 +15,6 @@ }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { - }, - "-12dB" : { - }, ": %@" : { diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index cbad8df9..eaf5db0b 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -391,6 +391,22 @@ "activelySupported": true, "displayName": "Heltec Vision Master E290" }, + { + "hwModel": 69, + "hwModelSlug": "HELTEC_MESH_NODE_T114", + "platformioTarget": "heltec-mesh-node-t114", + "architecture": "nrf52840", + "activelySupported": false, + "displayName": "Heltec Mesh Node T114" + }, + { + "hwModel": 70, + "hwModelSlug": "SENSECAP_INDICATOR", + "platformioTarget": "seeed-sensecap-indicator", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "SenseCAP Indicator" + }, { "hwModel": 71, "hwModelSlug": "TRACKER_T1000_E", diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 0b385205..c2a32870 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -26,16 +26,10 @@ struct TraceRouteLog: View { @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic let distanceFormatter = MKDistanceFormatter() - var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast - - /// Mockup Values - let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green] - let nums: [Int64] = [366311664, 0, 3662955168, 0, 3663982804, 0, 4202719792, 0, 603700594, 0, 836212501, 0, 3663116644, 0, 8362955168] - let snr: [Double] = [-115.00, 17.5, 7.0, 8.9, -24.0, 5.5, 6.0, 7.5] - @State private var hops: Int = 16 /// Max of 16 (2 8 hop routes) /// State for the circle of routes + var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast + @State private var indexes: Int = 0 @State var angle: Angle = .zero - //@State var radius: CGFloat = 175.00 @State var animation: Animation? var body: some View { @@ -99,6 +93,7 @@ struct TraceRouteLog: View { // Set the view rotation animation after the view appeared, // to avoid animating initial rotation DispatchQueue.main.async { + indexes = (selectedRoute?.hops?.array.count ?? 0) * 2 animation = .easeInOut(duration: 1.0) withAnimation(.easeInOut(duration: 2.0)) { angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) @@ -181,39 +176,28 @@ struct TraceRouteLog: View { }) } @ViewBuilder func contents(animation: Animation? = nil) -> some View { - ForEach(0.. Date: Tue, 24 Sep 2024 17:32:42 -0700 Subject: [PATCH 236/333] remove myinfo request --- Meshtastic/Helpers/BLEManager.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 63cf1883..dba3b26b 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -680,12 +680,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { disconnectPeripheral(reconnect: false) try container.restorePersistentStore(from: databasePath) - context.refreshAllObjects() - let request = MyInfoEntity.fetchRequest() - try context.fetch(request) UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0) - connectTo(peripheral: peripheral) + context.refreshAllObjects() Logger.data.notice("🗂️ Restored Core data for /\(UserDefaults.preferredPeripheralNum, privacy: .public)") + connectTo(peripheral: peripheral) } catch { Logger.data.error("🗂️ Restore Core data copy error: \(error, privacy: .public)") } From 1cf2aef8600489f732f10426d07b91a2d6a8accc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 25 Sep 2024 11:19:39 -0700 Subject: [PATCH 237/333] Remove mockup data --- Meshtastic/Views/Nodes/TraceRouteLog.swift | 62 ++++++++++++++++++---- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index c2a32870..45f0f1a1 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CoreData #if canImport(MapKit) import MapKit #endif @@ -77,15 +78,16 @@ struct TraceRouteLog: View { .font(idiom == .phone ? .headline : .largeTitle) } } - if true { // selectedRoute?.hops?.count ?? 2 > 3 { + if selectedRoute?.hops?.count ?? 2 > 3 { HStack(alignment: .center) { GeometryReader { geometry in - let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 50 : 70) + let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 45 : 85) Spacer() - TraceRoute(radius: size, rotation: angle) { + TraceRoute(radius: size < 600 ? size : 600, rotation: angle) { contents() } - .padding(.leading) + .padding(.leading, idiom == .phone ? 0 : 20) + Spacer() } .scaledToFit() } @@ -178,26 +180,66 @@ struct TraceRouteLog: View { @ViewBuilder func contents(animation: Animation? = nil) -> some View { ForEach(0.. [TraceRouteHopEntity] { + /// static let context = PersistenceController.preview.container.viewContext + var array = [TraceRouteHopEntity]() + let trh1 = TraceRouteHopEntity(context: context) + trh1.num = 366311664 + trh1.snr = 12.5 + let trh2 = TraceRouteHopEntity(context: context) + trh2.num = 3662955168 + trh2.snr = -115.00 + let trh3 = TraceRouteHopEntity(context: context) + trh3.num = 3663982804 + trh3.snr = 17.5 + let trh4 = TraceRouteHopEntity(context: context) + trh4.num = 4202719792 + trh4.snr = 7.0 + let trh5 = TraceRouteHopEntity(context: context) + trh5.num = 603700594 + trh5.snr = 8.9 + let trh6 = TraceRouteHopEntity(context: context) + trh6.num = 836212501 + trh6.snr = -24.0 + let trh7 = TraceRouteHopEntity(context: context) + trh7.num = 3663116644 + trh7.snr = -6.0 + let trh8 = TraceRouteHopEntity(context: context) + trh8.num = 8362955168 + trh8.snr = 7.5 + array.append(trh1) + array.append(trh2) + array.append(trh3) + array.append(trh4) + array.append(trh5) + array.append(trh6) + array.append(trh7) + array.append(trh8) + return array +} From 5024272c0be5fc167956fac6171bfe92c5045071 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 26 Sep 2024 16:48:18 -0700 Subject: [PATCH 238/333] Split trace route text into to and fro --- Localizable.xcstrings | 3 + .../CoreData/TraceRouteEntityExtension.swift | 30 ----- Meshtastic/Helpers/BLEManager.swift | 42 ++++--- .../contents | 5 +- Meshtastic/Views/Nodes/TraceRouteLog.swift | 115 ++++++++++-------- 5 files changed, 91 insertions(+), 104 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 95809869..aaa84ccd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -17579,6 +17579,9 @@ }, "Rotary 1" : { + }, + "Route Back: %@" : { + }, "Route Lines" : { diff --git a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift index 4e7cdb60..804aacf8 100644 --- a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift @@ -10,36 +10,6 @@ import CoreLocation import MapKit import SwiftUI -extension TraceRouteEntity { - - var latitude: Double? { - - let d = Double(latitudeI) - if d == 0 { - return 0 - } - return d / 1e7 - } - - var longitude: Double? { - - let d = Double(longitudeI) - if d == 0 { - return 0 - } - return d / 1e7 - } - - var coordinate: CLLocationCoordinate2D? { - if latitudeI != 0 && longitudeI != 0 { - let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) - return coord - } else { - return nil - } - } -} - extension TraceRouteHopEntity { var latitude: Double? { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 79a6525e..30b444e1 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -468,15 +468,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute.id = Int64(meshPacket.id) traceRoute.time = Date() traceRoute.node = receivingNode - // Grab the most recent postion, within the last hour - if connectedNode?.positions?.count ?? 0 > 0, let mostRecent = connectedNode?.positions?.lastObject as? PositionEntity { - if mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { - traceRoute.altitude = mostRecent.altitude - traceRoute.latitudeI = mostRecent.latitudeI - traceRoute.longitudeI = mostRecent.longitudeI - traceRoute.hasPositions = true - } - } do { try context.save() Logger.data.info("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized), privacy: .public)") @@ -840,21 +831,39 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if let routingMessage = try? RouteDiscovery(serializedBytes: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true - traceRoute?.route = routingMessage.route if routingMessage.route.count == 0 { let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from)) MeshLogger.log("🪧 \(logString)") - } else { - var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] + /// Add the connected node to the list of hops + var connectedHopNode = getNodeInfo(id: Int64(self.connectedPeripheral.num), context: context) + let connectedHop = TraceRouteHopEntity(context: context) + connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized + connectedHop.num = traceRoute?.num ?? 0 + connectedHop.time = Date() + if connectedHopNode?.hasPositions ?? false { + traceRoute?.hasPositions = true + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + connectedHop.altitude = mostRecent.altitude + connectedHop.latitudeI = mostRecent.latitudeI + connectedHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } else { + traceRoute?.hasPositions = false + } + } else { + traceRoute?.hasPositions = false + } + hopNodes.append(connectedHop) + var routeString = "You --> " for (index, node) in routingMessage.route.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) - traceRouteHop.time = Date() + traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) if hopNode?.hasPositions ?? false { traceRoute?.hasPositions = true @@ -876,8 +885,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) } - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") \(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB --> " + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } + var routeBackString = traceRoute?.node?.user?.longName ?? "unknown".localized for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { @@ -907,10 +917,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) } - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") --> " + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } - routeString += traceRoute?.node?.user?.longName ?? "unknown".localized traceRoute?.routeText = routeString + traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) do { try context.save() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index 265c23f8..5681bb76 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -413,14 +413,11 @@ - - - - + diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 45f0f1a1..03c2b5ba 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -55,7 +55,14 @@ struct TraceRouteLog: View { Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { - Image(systemName: "signpost.right.and.left") + Image(systemName: "signpost.right") + .symbolRenderingMode(.hierarchical) + } + .font(.title3) + Label { + Text("Route Back: \(selectedRoute?.routeBackText ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.left") .symbolRenderingMode(.hierarchical) } .font(.title3) @@ -78,7 +85,7 @@ struct TraceRouteLog: View { .font(idiom == .phone ? .headline : .largeTitle) } } - if selectedRoute?.hops?.count ?? 2 > 3 { + if selectedRoute?.hops?.count ?? 0 >= 3 { HStack(alignment: .center) { GeometryReader { geometry in let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 45 : 85) @@ -109,58 +116,58 @@ struct TraceRouteLog: View { } } if selectedRoute?.hasPositions ?? false { - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - .annotationTitles(.automatic) - // Direct Trace Route - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { - if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] - Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.black)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - let dashed = StrokeStyle( - lineWidth: 2, - lineCap: .round, lineJoin: .round, dash: [7, 10] - ) - MapPolyline(coordinates: traceRouteCoords) - .stroke(.blue, style: dashed) - } - } - } - .frame(maxWidth: .infinity, minHeight: 250) - if selectedRoute?.response ?? false { - VStack { - /// Distance - if selectedRoute?.node?.positions?.count ?? 0 > 0, - selectedRoute?.coordinate != nil, - let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) - if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) - Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") - .foregroundColor(.primary) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - } - } - } - } - } +// Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { +// Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.green)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// .annotationTitles(.automatic) +// // Direct Trace Route +// if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { +// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { +// let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] +// Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.black)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// let dashed = StrokeStyle( +// lineWidth: 2, +// lineCap: .round, lineJoin: .round, dash: [7, 10] +// ) +// MapPolyline(coordinates: traceRouteCoords) +// .stroke(.blue, style: dashed) +// } +// } +// } +// .frame(maxWidth: .infinity, minHeight: 250) +// if selectedRoute?.response ?? false { +// VStack { +// /// Distance +// if selectedRoute?.node?.positions?.count ?? 0 > 0, +// selectedRoute?.coordinate != nil, +// let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { +// let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) +// if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { +// let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) +// Label { +// Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") +// .foregroundColor(.primary) +// } icon: { +// Image(systemName: "lines.measurement.horizontal") +// .symbolRenderingMode(.hierarchical) +// } +// } +// } +// } +// } Spacer() .padding(.bottom, 125) } From ae2bc0c8829674873df35df2b67f947c1c0f675d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 26 Sep 2024 17:21:29 -0700 Subject: [PATCH 239/333] Show map more often --- Meshtastic/Helpers/BLEManager.swift | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 30b444e1..805cfc01 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -872,11 +872,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized - } else { - traceRoute?.hasPositions = false + traceRoute?.hasPositions = true } - } else { - traceRoute?.hasPositions = false } traceRouteHop.num = hopNode?.num ?? 0 if hopNode != nil { @@ -904,11 +901,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized - } else { - traceRoute?.hasPositions = false + traceRoute?.hasPositions = true } - } else { - traceRoute?.hasPositions = false } traceRouteHop.num = hopNode?.num ?? 0 if hopNode != nil { From 7e5ee3c4a47e8c0f3d786a5115d964d0b68d41c9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 27 Sep 2024 08:35:57 -0700 Subject: [PATCH 240/333] clean up positions in traceroutes --- Meshtastic/Helpers/BLEManager.swift | 21 +++++++------------ .../contents | 1 - 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 805cfc01..cb558f01 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -837,23 +837,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { var hopNodes: [TraceRouteHopEntity] = [] /// Add the connected node to the list of hops - var connectedHopNode = getNodeInfo(id: Int64(self.connectedPeripheral.num), context: context) + let connectedHopNode = getNodeInfo(id: Int64(self.connectedPeripheral.num), context: context) let connectedHop = TraceRouteHopEntity(context: context) connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized - connectedHop.num = traceRoute?.num ?? 0 connectedHop.time = Date() if connectedHopNode?.hasPositions ?? false { - traceRoute?.hasPositions = true - if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI connectedHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true - } else { - traceRoute?.hasPositions = false } - } else { - traceRoute?.hasPositions = false } hopNodes.append(connectedHop) var routeString = "You --> " @@ -866,8 +860,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) if hopNode?.hasPositions ?? false { - traceRoute?.hasPositions = true - if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI @@ -895,8 +888,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.back = true traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) if hopNode?.hasPositions ?? false { - traceRoute?.hasPositions = true - if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI @@ -911,7 +903,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) } - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " + routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString @@ -952,7 +944,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)") - sendTime() + if sendTime() { + } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index 5681bb76..e3fb02a1 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -415,7 +415,6 @@ - From bb5320035bccc7753bf0babd0613dc15aca484e6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 29 Sep 2024 10:25:37 -0700 Subject: [PATCH 241/333] Fix potential traceroute position crashes --- .../CoreData/NodeInfoEntityExtension.swift | 2 +- Meshtastic/Helpers/BLEManager.swift | 16 ++++++++-------- .../MeshtasticDataModelV 45.xcdatamodel/contents | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index c1bd5bec..7585fb1e 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -23,7 +23,7 @@ extension NodeInfoEntity { } var hasPositions: Bool { - return positions?.count ?? 0 > 0 + return self.positions?.count ?? 0 > 0 } var hasDeviceMetrics: Bool { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index cb558f01..83b37dc7 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -832,7 +832,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true if routingMessage.route.count == 0 { - let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from)) + let snr = routingMessage.snrBack.count > 0 ? routingMessage.snrBack[0] / 4 : 0 + traceRoute?.snr = Float(snr) + let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("🪧 \(logString)") } else { var hopNodes: [TraceRouteHopEntity] = [] @@ -841,7 +843,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let connectedHop = TraceRouteHopEntity(context: context) connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized connectedHop.time = Date() - if connectedHopNode?.hasPositions ?? false { + if let cn = connectedHopNode, cn.hasPositions { if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI @@ -859,12 +861,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) - if hopNode?.hasPositions ?? false { - if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + if let hn = hopNode, hn.hasPositions { + if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI - traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized traceRoute?.hasPositions = true } } @@ -887,12 +888,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.time = Date() traceRouteHop.back = true traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) - if hopNode?.hasPositions ?? false { - if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + if let hn = hopNode, hn.hasPositions { + if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI - traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized traceRoute?.hasPositions = true } } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index e3fb02a1..c16a68b0 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -418,6 +418,7 @@ + From 02ff0605333dacc60d467c3b20d270be06ee6d7d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 29 Sep 2024 10:40:49 -0700 Subject: [PATCH 242/333] remove extra node lookup --- Meshtastic/Helpers/BLEManager.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 83b37dc7..0ffb0af2 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -838,18 +838,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🪧 \(logString)") } else { var hopNodes: [TraceRouteHopEntity] = [] - /// Add the connected node to the list of hops - let connectedHopNode = getNodeInfo(id: Int64(self.connectedPeripheral.num), context: context) let connectedHop = TraceRouteHopEntity(context: context) connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized connectedHop.time = Date() - if let cn = connectedHopNode, cn.hasPositions { - if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { - connectedHop.altitude = mostRecent.altitude - connectedHop.latitudeI = mostRecent.latitudeI - connectedHop.longitudeI = mostRecent.longitudeI - traceRoute?.hasPositions = true - } + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + connectedHop.altitude = mostRecent.altitude + connectedHop.latitudeI = mostRecent.latitudeI + connectedHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true } hopNodes.append(connectedHop) var routeString = "You --> " @@ -859,7 +855,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) - traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { From 01c22745e35234e7ebc544da8ce6071256f3883e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 30 Sep 2024 08:23:47 -0700 Subject: [PATCH 243/333] Assorted cleanup --- Localizable.xcstrings | 11 +++++- Meshtastic/Helpers/BLEManager.swift | 29 ++++++++------ Meshtastic/Helpers/LocationsHandler.swift | 6 +-- .../contents | 1 + Meshtastic/Views/Nodes/TraceRouteLog.swift | 39 ++++++++++++++----- .../Config/Module/StoreForwardConfig.swift | 2 +- 6 files changed, 63 insertions(+), 25 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index aaa84ccd..11d7c54b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -194,6 +194,9 @@ }, "256 bit" : { + }, + "A Trace Route was sent, no response has been received." : { + }, "about" : { "localizations" : { @@ -20156,7 +20159,7 @@ "Store and forward clients can request history from routers on the network." : { }, - "Store and forward router devices must also be in the router or router client device role and requires a ESP32 device with PSRAM." : { + "Store and forward router devices require a ESP32 device with PSRAM." : { }, "storeforward" : { @@ -21874,6 +21877,12 @@ }, "Trace route sent to %@" : { + }, + "Trace route to %@ was not sent." : { + + }, + "Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds." : { + }, "Traffic" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0ffb0af2..bb1cb8c4 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -592,7 +592,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } do { - let logRecord = try LogRecord(serializedData: characteristic.value!) + let logRecord = try LogRecord(serializedBytes: characteristic.value!) var message = logRecord.source.isEmpty ? logRecord.message : "[\(logRecord.source)] \(logRecord.message)" switch logRecord.level { case .debug: @@ -613,14 +613,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Ignore fail to parse as LogRecord } - case LEGACY_LOGRADIO_UUID: - if characteristic.value == nil || characteristic.value!.isEmpty { - return - } - if let log = String(data: characteristic.value!, encoding: .utf8) { - handleRadioLog(radioLog: log) - } - case FROMRADIO_UUID: if characteristic.value == nil || characteristic.value!.isEmpty { @@ -629,7 +621,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var decodedInfo = FromRadio() do { - decodedInfo = try FromRadio(serializedData: characteristic.value!) + decodedInfo = try FromRadio(serializedBytes: characteristic.value!) } catch { Logger.services.error("💥 \(error.localizedDescription, privacy: .public) \(characteristic.value!, privacy: .public)") @@ -644,6 +636,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { + if decodedInfo.clientNotification.hasReplyID { + /// Set Sent bool on TraceRouteEntity to false if we got rate limited + if decodedInfo.clientNotification.message.starts(with: "TraceRoute") { + let traceRoute = getTraceRoute(id: Int64(decodedInfo.clientNotification.replyID), context: context) + traceRoute?.sent = false + do { + try context.save() + Logger.data.info("💾 [TraceRouteEntity] Trace Route Rate Limited") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } + } let manager = LocalNotificationManager() manager.notifications = [ Notification( @@ -916,7 +923,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } case .neighborinfoApp: - if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { + if let neighborInfo = try? NeighborInfo(serializedBytes: decodedInfo.packet.decoded.payload) { // MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED") MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 9b16e3de..20d7725f 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -85,15 +85,15 @@ import OSLog if smartPostion { let age = -location.timestamp.timeIntervalSinceNow if age > 10 { - Logger.services.warning("📍 [App] Bad Location \(self.count, privacy: .public): Too Old \(age, privacy: .public) seconds ago \(location, privacy: .private)") + Logger.services.info("📍 [App] Smart Position - Bad Location: Too Old \(age, privacy: .public) seconds ago \(location, privacy: .private)") return false } if location.horizontalAccuracy < 0 { - Logger.services.warning("📍 [App] Bad Location \(self.count, privacy: .public): Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") + Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") return false } if location.horizontalAccuracy > 5 { - Logger.services.warning("📍 [App] Bad Location \(self.count, privacy: .public): Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") + Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") return false } } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index c16a68b0..65a05ff9 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -418,6 +418,7 @@ + diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 03c2b5ba..f32cbc81 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -39,7 +39,8 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")") + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : (route.sent ? "No Response" : "Not Sent"))") + .font(.callout) } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) @@ -47,7 +48,7 @@ struct TraceRouteLog: View { } .listStyle(.plain) } - .frame(minHeight: CGFloat(node.traceRoutes?.count ?? 0 * 40), maxHeight: 150) + .frame(minHeight: CGFloat(node.traceRoutes?.count ?? 0 * 40), maxHeight: 250) Divider() ScrollView { if selectedRoute != nil { @@ -74,16 +75,36 @@ struct TraceRouteLog: View { .symbolRenderingMode(.hierarchical) } .font(.title3) + } else if !(selectedRoute?.sent ?? true) { + Label { + VStack { + Text("Trace route to \(selectedRoute?.node?.user?.longName ?? "unknown".localized) was not sent.") + .font(idiom == .phone ? .body : .largeTitle) + .fontWeight(.semibold) + Text("Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds.") + .font(idiom == .phone ? .caption : .body) + .foregroundStyle(.secondary) + .padding() + } + } icon: { + Image(systemName: "square.and.arrow.up.trianglebadge.exclamationmark") + .symbolRenderingMode(.hierarchical) + } } else { - VStack { - Label { + Label { + VStack { Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .body : .largeTitle) + .fontWeight(.semibold) + Text("A Trace Route was sent, no response has been received.") + .font(idiom == .phone ? .caption : .body) + .foregroundStyle(.secondary) + .padding() } - .font(idiom == .phone ? .headline : .largeTitle) - } + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } } if selectedRoute?.hops?.count ?? 0 >= 3 { HStack(alignment: .center) { diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 8fff8979..aeee9601 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -56,7 +56,7 @@ struct StoreForwardConfig: View { } VStack { if isRouter { - Text("Store and forward router devices must also be in the router or router client device role and requires a ESP32 device with PSRAM.") + Text("Store and forward router devices require a ESP32 device with PSRAM.") .foregroundColor(.gray) .font(.callout) } else { From 2d4373e2f00e195e431ec8e3eda5a6c08e1c28b6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 30 Sep 2024 12:05:32 -0700 Subject: [PATCH 244/333] fix snr error --- Meshtastic/Helpers/BLEManager.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index bb1cb8c4..865f8d23 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -862,7 +862,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) - traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) + if routingMessage.snrTowards.count >= index + 1 { + traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) + } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude @@ -889,7 +891,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() traceRouteHop.back = true - traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) + if routingMessage.snrBack.count >= index + 1 { + traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) + } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude From dc3766611480ea574ff2f9359967a7d7de3c7bdf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 4 Oct 2024 16:25:03 -0700 Subject: [PATCH 245/333] update protof --- .../Sources/meshtastic/atak.pb.swift | 32 +++++++++++ .../Sources/meshtastic/clientonly.pb.swift | 54 +++++++++++++++++++ .../Sources/meshtastic/deviceonly.pb.swift | 14 +++-- .../Sources/meshtastic/mesh.pb.swift | 40 ++++++++++++-- protobufs | 2 +- 5 files changed, 131 insertions(+), 11 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 4406deb3..c756d94d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -322,6 +322,17 @@ public struct TAKPacket { set {payloadVariant = .chat(newValue)} } + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender + public var detail: Data { + get { + if case .detail(let v)? = payloadVariant {return v} + return Data() + } + set {payloadVariant = .detail(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -333,6 +344,10 @@ public struct TAKPacket { /// /// ATAK GeoChat message case chat(GeoChat) + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender + case detail(Data) #if !swift(>=4.1) public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { @@ -348,6 +363,10 @@ public struct TAKPacket { guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } return l == r }() + case (.detail, .detail): return { + guard case .detail(let l) = lhs, case .detail(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -555,6 +574,7 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 4: .same(proto: "status"), 5: .same(proto: "pli"), 6: .same(proto: "chat"), + 7: .same(proto: "detail"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -593,6 +613,14 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.payloadVariant = .chat(v) } }() + case 7: try { + var v: Data? + try decoder.decodeSingularBytesField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .detail(v) + } + }() default: break } } @@ -624,6 +652,10 @@ extension TAKPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .chat(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 6) }() + case .detail?: try { + guard case .detail(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBytesField(value: v, fieldNumber: 7) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index c3d93bf7..f89a8e3c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -83,6 +83,39 @@ public struct DeviceProfile { /// Clears the value of `moduleConfig`. Subsequent reads from it will return its default value. public mutating func clearModuleConfig() {self._moduleConfig = nil} + /// + /// Fixed position data + public var fixedPosition: Position { + get {return _fixedPosition ?? Position()} + set {_fixedPosition = newValue} + } + /// Returns true if `fixedPosition` has been explicitly set. + public var hasFixedPosition: Bool {return self._fixedPosition != nil} + /// Clears the value of `fixedPosition`. Subsequent reads from it will return its default value. + public mutating func clearFixedPosition() {self._fixedPosition = nil} + + /// + /// Ringtone for ExternalNotification + public var ringtone: String { + get {return _ringtone ?? String()} + set {_ringtone = newValue} + } + /// Returns true if `ringtone` has been explicitly set. + public var hasRingtone: Bool {return self._ringtone != nil} + /// Clears the value of `ringtone`. Subsequent reads from it will return its default value. + public mutating func clearRingtone() {self._ringtone = nil} + + /// + /// Predefined messages for CannedMessage + public var cannedMessages: String { + get {return _cannedMessages ?? String()} + set {_cannedMessages = newValue} + } + /// Returns true if `cannedMessages` has been explicitly set. + public var hasCannedMessages: Bool {return self._cannedMessages != nil} + /// Clears the value of `cannedMessages`. Subsequent reads from it will return its default value. + public mutating func clearCannedMessages() {self._cannedMessages = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -92,6 +125,9 @@ public struct DeviceProfile { fileprivate var _channelURL: String? = nil fileprivate var _config: LocalConfig? = nil fileprivate var _moduleConfig: LocalModuleConfig? = nil + fileprivate var _fixedPosition: Position? = nil + fileprivate var _ringtone: String? = nil + fileprivate var _cannedMessages: String? = nil } #if swift(>=5.5) && canImport(_Concurrency) @@ -110,6 +146,9 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa 3: .standard(proto: "channel_url"), 4: .same(proto: "config"), 5: .standard(proto: "module_config"), + 6: .standard(proto: "fixed_position"), + 7: .same(proto: "ringtone"), + 8: .standard(proto: "canned_messages"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -123,6 +162,9 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa case 3: try { try decoder.decodeSingularStringField(value: &self._channelURL) }() case 4: try { try decoder.decodeSingularMessageField(value: &self._config) }() case 5: try { try decoder.decodeSingularMessageField(value: &self._moduleConfig) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._fixedPosition) }() + case 7: try { try decoder.decodeSingularStringField(value: &self._ringtone) }() + case 8: try { try decoder.decodeSingularStringField(value: &self._cannedMessages) }() default: break } } @@ -148,6 +190,15 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa try { if let v = self._moduleConfig { try visitor.visitSingularMessageField(value: v, fieldNumber: 5) } }() + try { if let v = self._fixedPosition { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + try { if let v = self._ringtone { + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + } }() + try { if let v = self._cannedMessages { + try visitor.visitSingularStringField(value: v, fieldNumber: 8) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -157,6 +208,9 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if lhs._channelURL != rhs._channelURL {return false} if lhs._config != rhs._config {return false} if lhs._moduleConfig != rhs._moduleConfig {return false} + if lhs._fixedPosition != rhs._fixedPosition {return false} + if lhs._ringtone != rhs._ringtone {return false} + if lhs._cannedMessages != rhs._cannedMessages {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 43506399..3dd965f2 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -235,9 +235,13 @@ public struct NodeInfoLite { /// /// Number of hops away from us this node is (0 if adjacent) public var hopsAway: UInt32 { - get {return _storage._hopsAway} + get {return _storage._hopsAway ?? 0} set {_uniqueStorage()._hopsAway = newValue} } + /// Returns true if `hopsAway` has been explicitly set. + public var hasHopsAway: Bool {return _storage._hopsAway != nil} + /// Clears the value of `hopsAway`. Subsequent reads from it will return its default value. + public mutating func clearHopsAway() {_uniqueStorage()._hopsAway = nil} /// /// True if node is in our favorites list @@ -620,7 +624,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _deviceMetrics: DeviceMetrics? = nil var _channel: UInt32 = 0 var _viaMqtt: Bool = false - var _hopsAway: UInt32 = 0 + var _hopsAway: UInt32? = nil var _isFavorite: Bool = false #if swift(>=5.10) @@ -710,9 +714,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._viaMqtt != false { try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 8) } - if _storage._hopsAway != 0 { - try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) - } + try { if let v = _storage._hopsAway { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() if _storage._isFavorite != false { try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index c7e8dc07..6bddec60 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1416,6 +1416,14 @@ public struct Routing { /// /// The receiving node does not have a Public Key to decode with case pkiUnknownPubkey // = 35 + + /// + /// Admin packet otherwise checks out, but uses a bogus or expired session key + case adminBadSessionKey // = 36 + + /// + /// Admin packet sent using PKC, but not from a public key on the admin key list + case adminPublicKeyUnauthorized // = 37 case UNRECOGNIZED(Int) public init() { @@ -1438,6 +1446,8 @@ public struct Routing { case 33: self = .notAuthorized case 34: self = .pkiFailed case 35: self = .pkiUnknownPubkey + case 36: self = .adminBadSessionKey + case 37: self = .adminPublicKeyUnauthorized default: self = .UNRECOGNIZED(rawValue) } } @@ -1458,6 +1468,8 @@ public struct Routing { case .notAuthorized: return 33 case .pkiFailed: return 34 case .pkiUnknownPubkey: return 35 + case .adminBadSessionKey: return 36 + case .adminPublicKeyUnauthorized: return 37 case .UNRECOGNIZED(let i): return i } } @@ -1486,6 +1498,8 @@ extension Routing.Error: CaseIterable { .notAuthorized, .pkiFailed, .pkiUnknownPubkey, + .adminBadSessionKey, + .adminPublicKeyUnauthorized, ] } @@ -2167,9 +2181,13 @@ public struct NodeInfo { /// /// Number of hops away from us this node is (0 if adjacent) public var hopsAway: UInt32 { - get {return _storage._hopsAway} + get {return _storage._hopsAway ?? 0} set {_uniqueStorage()._hopsAway = newValue} } + /// Returns true if `hopsAway` has been explicitly set. + public var hasHopsAway: Bool {return _storage._hopsAway != nil} + /// Clears the value of `hopsAway`. Subsequent reads from it will return its default value. + public mutating func clearHopsAway() {_uniqueStorage()._hopsAway = nil} /// /// True if node is in our favorites list @@ -2997,6 +3015,10 @@ public struct DeviceMetadata { /// Has Remote Hardware enabled public var hasRemoteHardware_p: Bool = false + /// + /// Has PKC capabilities + public var hasPkc_p: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -3818,6 +3840,8 @@ extension Routing.Error: SwiftProtobuf._ProtoNameProviding { 33: .same(proto: "NOT_AUTHORIZED"), 34: .same(proto: "PKI_FAILED"), 35: .same(proto: "PKI_UNKNOWN_PUBKEY"), + 36: .same(proto: "ADMIN_BAD_SESSION_KEY"), + 37: .same(proto: "ADMIN_PUBLIC_KEY_UNAUTHORIZED"), ] } @@ -4326,7 +4350,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _deviceMetrics: DeviceMetrics? = nil var _channel: UInt32 = 0 var _viaMqtt: Bool = false - var _hopsAway: UInt32 = 0 + var _hopsAway: UInt32? = nil var _isFavorite: Bool = false #if swift(>=5.10) @@ -4416,9 +4440,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._viaMqtt != false { try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 8) } - if _storage._hopsAway != 0 { - try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) - } + try { if let v = _storage._hopsAway { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } }() if _storage._isFavorite != false { try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) } @@ -5281,6 +5305,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 8: .standard(proto: "position_flags"), 9: .standard(proto: "hw_model"), 10: .same(proto: "hasRemoteHardware"), + 11: .same(proto: "hasPKC"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -5299,6 +5324,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.hasRemoteHardware_p) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.hasPkc_p) }() default: break } } @@ -5335,6 +5361,9 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.hasRemoteHardware_p != false { try visitor.visitSingularBoolField(value: self.hasRemoteHardware_p, fieldNumber: 10) } + if self.hasPkc_p != false { + try visitor.visitSingularBoolField(value: self.hasPkc_p, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -5349,6 +5378,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if lhs.positionFlags != rhs.positionFlags {return false} if lhs.hwModel != rhs.hwModel {return false} if lhs.hasRemoteHardware_p != rhs.hasRemoteHardware_p {return false} + if lhs.hasPkc_p != rhs.hasPkc_p {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index 0acaec6e..5709c0a0 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 0acaec6eff00e748beeae89148093221f131cd9c +Subproject commit 5709c0a05eaefccbc9cb8ed3917adbf5fd134197 From 1adf9d9d5887074ad7a2e4fd2c46175f5e33d888 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 4 Oct 2024 18:28:42 -0700 Subject: [PATCH 246/333] Delete for traceroutes --- .../contents | 2 +- Meshtastic/Views/Nodes/TraceRouteLog.swift | 15 ++++- .../Views/Settings/Logs/LogDetail.swift | 61 +++++++++++-------- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index 65a05ff9..2a5e4a56 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -421,7 +421,7 @@ - + diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index f32cbc81..f0828192 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -7,6 +7,7 @@ import SwiftUI import CoreData +import OSLog #if canImport(MapKit) import MapKit #endif @@ -45,6 +46,18 @@ struct TraceRouteLog: View { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) } + .swipeActions { + Button(role: .destructive) { + context.delete(route) + do { + try context.save() + } catch let error as NSError { + Logger.data.error("\(error.localizedDescription)") + } + } label: { + Label("delete", systemImage: "trash") + } + } } .listStyle(.plain) } @@ -208,7 +221,7 @@ struct TraceRouteLog: View { @ViewBuilder func contents(animation: Animation? = nil) -> some View { ForEach(0.. Date: Fri, 4 Oct 2024 19:36:30 -0700 Subject: [PATCH 247/333] Upgrade to ios 17 --- Localizable.xcstrings | 6 ++++++ Meshtastic.xcodeproj/project.pbxproj | 8 +++---- .../xcschemes/Meshtastic.xcscheme | 2 +- .../xcschemes/WidgetsExtension.xcscheme | 2 +- .../AppIntents/MessageChannelIntent.swift | 21 +++++++++---------- .../AppIntents/SendWaypointIntent.swift | 16 ++++++-------- Meshtastic/Helpers/LocationsHandler.swift | 1 - Meshtastic/Helpers/MeshPackets.swift | 2 +- Meshtastic/MeshtasticApp.swift | 2 -- Meshtastic/Tips/BluetoothTips.swift | 3 --- Meshtastic/Tips/ChannelTips.swift | 5 ----- Meshtastic/Tips/MessagesTips.swift | 3 --- Meshtastic/Views/Bluetooth/Connect.swift | 2 -- Meshtastic/Views/Messages/Messages.swift | 2 -- Meshtastic/Views/Messages/UserList.swift | 2 -- .../Map/MapContent/NodeMapContent.swift | 1 - .../Nodes/Helpers/Map/MapSettingsForm.swift | 3 --- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 3 --- .../Helpers/Map/PositionAltitudeChart.swift | 3 --- .../Nodes/Helpers/Map/PositionPopover.swift | 1 - Meshtastic/Views/Nodes/MeshMap.swift | 3 --- Meshtastic/Views/Nodes/TraceRouteLog.swift | 4 +--- Meshtastic/Views/Settings/AppLog.swift | 3 --- Meshtastic/Views/Settings/Channels.swift | 2 -- .../Views/Settings/Channels/ChannelForm.swift | 2 -- .../Views/Settings/Config/DeviceConfig.swift | 14 +++++++++++-- .../Config/Module/AmbientLightingConfig.swift | 1 - Meshtastic/Views/Settings/GPSStatus.swift | 1 - Meshtastic/Views/Settings/RouteRecorder.swift | 1 - Meshtastic/Views/Settings/Routes.swift | 1 - Meshtastic/Views/Settings/Settings.swift | 2 -- Meshtastic/Views/Settings/ShareChannels.swift | 3 --- 32 files changed, 41 insertions(+), 84 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a61ea1cf..f17521af 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -19163,6 +19163,9 @@ }, "Send a message to a certain meshtastic channel" : { + }, + "Send a position on the primary channel when the user button is triple clicked." : { + }, "Send a shutdown to the node you are connected to" : { @@ -21906,6 +21909,9 @@ }, "Treat double tap on supported accelerometers as a user button press." : { + }, + "Triple Click Ad Hoc Ping" : { + }, "Try Again" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7d9209f1..2c215a85 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1153,7 +1153,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1540; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1600; TargetAttributes = { 25F5D5C62C4375A8008036E3 = { CreatedOnToolsVersion = 15.4; @@ -1667,7 +1667,6 @@ DDC2E17F26CE248F0042C5E4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; @@ -1682,7 +1681,7 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1702,7 +1701,6 @@ DDC2E18026CE248F0042C5E4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; @@ -1717,7 +1715,7 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme index 9ef67c6d..19c6089f 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme @@ -1,6 +1,6 @@ some IntentResult { - if (!BLEManager.shared.isConnected){ + if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected } - + // Check if channel number is between 1 and 7 guard (0...7).contains(channelNumber) else { throw $channelNumber.needsValueError("Channel number must be between 0 and 7.") } - + // Convert messageContent to data and check its length guard let messageData = messageContent.data(using: .utf8) else { throw AppIntentErrors.AppIntentError.message("Failed to encode message content") } - + if messageData.count > 228 { throw $messageContent.needsValueError("Message content exceeds 228 bytes.") } - if(!BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0)){ + if !BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) { throw AppIntentErrors.AppIntentError.message("Failed to send message") } - - return .result() + + return .result() } } diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 392d232a..4352c548 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -16,10 +16,10 @@ struct SendWaypointIntent: AppIntent { @Parameter(title: "Name", default: "Dropped Pin") var nameParameter: String? - + @Parameter(title: "Description", default: "") var descriptionParameter: String? - + @Parameter(title: "Emoji", default: "📍") var emojiParameter: String? @@ -27,19 +27,19 @@ struct SendWaypointIntent: AppIntent { var locationParameter: CLPlacemark func perform() async throws -> some IntentResult { - if (!BLEManager.shared.isConnected){ + if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected } // Provide default values if parameters are nil let name = nameParameter ?? "Dropped Pin" let description = descriptionParameter ?? "" let emoji = emojiParameter ?? "📍" - + // Validate name length if name.utf8.count > 30 { throw $nameParameter.needsValueError("Name must be less than 30 bytes") } - + // Validate description length if description.utf8.count > 100 { throw $descriptionParameter.needsValueError("Description must be less than 100 bytes") @@ -60,7 +60,6 @@ struct SendWaypointIntent: AppIntent { newWaypoint.longitudeI = Int32(longitude * 10_000_000) } - newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. } -@available(iOS 17.0, macOS 14.0, *) struct PositionAltitudeChart: View { @Environment(\.dismiss) private var dismiss @ObservedObject var node: NodeInfoEntity diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 3f0a528b..35406707 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -8,7 +8,6 @@ import SwiftUI import MapKit -@available(iOS 17.0, macOS 14.0, *) struct PositionPopover: View { @ObservedObject var locationsHandler = LocationsHandler.shared diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 595abb15..da9001ea 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -10,11 +10,8 @@ import CoreData import CoreLocation import Foundation import OSLog -#if canImport(MapKit) import MapKit -#endif -@available(iOS 17.0, macOS 14.0, *) struct MeshMap: View { @Environment(\.managedObjectContext) var context diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 345a4299..8166b15f 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -6,11 +6,9 @@ // import SwiftUI -#if canImport(MapKit) import MapKit -#endif -@available(iOS 17.0, macOS 14.0, *) + struct TraceRouteLog: View { @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index fd3db810..e1b2e15c 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -8,8 +8,6 @@ import SwiftUI import OSLog -/// Needed for TableColumnForEach -@available(iOS 17.0, macOS 14.0, *) struct AppLog: View { @State private var logs: [OSLogEntryLog] = [] @@ -216,7 +214,6 @@ struct AppLog: View { } } -@available(iOS 17.0, macOS 14.0, *) extension AppLog { @MainActor private func searchAppLogs() async -> [OSLogEntryLog] { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index ca55b95d..fbc9f75b 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -10,9 +10,7 @@ import MapKit import MeshtasticProtobufs import OSLog import SwiftUI -#if canImport(TipKit) import TipKit -#endif func generateChannelKey(size: Int) -> String { var keyData = Data(count: size) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index e4930b7a..b4943db7 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -6,9 +6,7 @@ // import SwiftUI -#if canImport(MapKit) import MapKit -#endif struct ChannelForm: View { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index c78cb845..55359045 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -28,6 +28,7 @@ struct DeviceConfig: View { @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @State var ledHeartbeatEnabled = true + @State var tripleClickAsAdHocPing = true @State var tzdef = "" var body: some View { @@ -76,6 +77,12 @@ struct DeviceConfig: View { Text("Treat double tap on supported accelerometers as a user button press.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + Toggle(isOn: $tripleClickAsAdHocPing) { + Label("Triple Click Ad Hoc Ping", systemImage: "map.pin") + Text("Send a position on the primary channel when the user button is triple clicked.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $ledHeartbeatEnabled) { Label("LED Heartbeat", systemImage: "waveform.path.ecg") @@ -93,13 +100,13 @@ struct DeviceConfig: View { Label("Time Zone", systemImage: "clock.badge.exclamationmark") TextField("Time Zone", text: $tzdef, axis: .vertical) .foregroundColor(.gray) - .onChange(of: tzdef, perform: { _ in + .onChange(of: tzdef) { let totalBytes = tzdef.utf8.count // Only mess with the value if it is too big if totalBytes > 63 { tzdef = String(tzdef.dropLast()) } - }) + } .foregroundColor(.gray) } @@ -268,6 +275,9 @@ struct DeviceConfig: View { .onChange(of: doubleTapAsButtonPress) { if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } } + .onChange(of: tripleClickAsAdHocPing) { + // if $0 != node?.deviceConfig?.tripleClickAsAdHocPing { hasChanges = true } + } .onChange(of: tzdef) { newTzdef in if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } } diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index fe3faa51..5e82da1b 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -8,7 +8,6 @@ import MeshtasticProtobufs import SwiftUI import OSLog -@available(iOS 17.0, macOS 14.0, *) struct AmbientLightingConfig: View { @Environment(\.self) var environment @Environment(\.managedObjectContext) var context diff --git a/Meshtastic/Views/Settings/GPSStatus.swift b/Meshtastic/Views/Settings/GPSStatus.swift index b1119694..c92a647c 100644 --- a/Meshtastic/Views/Settings/GPSStatus.swift +++ b/Meshtastic/Views/Settings/GPSStatus.swift @@ -8,7 +8,6 @@ import SwiftUI import CoreLocation -@available(iOS 17.0, macOS 14.0, *) struct GPSStatus: View { var largeFont: Font = .footnote diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index c19dbd9f..ea2cdc41 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -12,7 +12,6 @@ import CoreLocation import CoreMotion import OSLog -@available(iOS 17.0, macOS 14.0, *) struct RouteRecorder: View { @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 65be4fd3..c5bcb69c 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -10,7 +10,6 @@ import CoreData import MapKit import OSLog -@available(iOS 17.0, macOS 14.0, *) struct Routes: View { @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 481fb29d..6176512f 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -7,9 +7,7 @@ import SwiftUI import OSLog -#if canImport(TipKit) import TipKit -#endif struct Settings: View { @Environment(\.managedObjectContext) var context diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 136b9563..f4f4167a 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -8,10 +8,7 @@ import SwiftUI import CoreData import CoreImage.CIFilterBuiltins import MeshtasticProtobufs - -#if canImport(TipKit) import TipKit -#endif struct QrCodeImage { let context = CIContext() From aee1a31e8e43af35fe835915922ff7f08995af7a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Oct 2024 09:53:33 -0700 Subject: [PATCH 248/333] add packet filter for message lock icon --- Meshtastic/Helpers/MeshPackets.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 75d2b85d..7d0c6a6b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -876,7 +876,7 @@ func textMessageAppPacket( if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) /// Set the public key for the message - if newMessage.fromUser?.pkiEncrypted ?? false { + if newMessage.fromUser?.pkiEncrypted ?? false && packet.pkiEncrypted { newMessage.pkiEncrypted = true newMessage.publicKey = packet.publicKey } From 6f07eea9f8e6fc4f93cd4b78f297f3456fe5611c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Oct 2024 09:55:08 -0700 Subject: [PATCH 249/333] Remove testing data --- Meshtastic/Views/Nodes/TraceRouteLog.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index f0828192..22d190d5 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -221,7 +221,7 @@ struct TraceRouteLog: View { @ViewBuilder func contents(animation: Animation? = nil) -> some View { ForEach(0.. Date: Sat, 5 Oct 2024 10:19:32 -0700 Subject: [PATCH 250/333] 2.5 is now a beta --- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 4 ++-- Meshtastic/Views/Settings/Firmware.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 4a0e7234..fd637659 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -100,7 +100,7 @@ struct DeviceConfig: View { Label("Time Zone", systemImage: "clock.badge.exclamationmark") TextField("Time Zone", text: $tzdef, axis: .vertical) .foregroundColor(.gray) - .onChange(of: tzdef, perform: { _ in + .onChange(of: tzdef) { _ in var totalBytes = tzdef.utf8.count // Only mess with the value if it is too big while totalBytes > 63 { @@ -114,7 +114,7 @@ struct DeviceConfig: View { .disableAutocorrection(true) Text("Time zone for dates on the device screen and log.") .foregroundColor(.gray) - .font(.callout) + .font(.callout) } } Section(header: Text("GPIO")) { diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 9a450775..9962e69d 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -13,7 +13,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.4.2" + @State var minimumVersion = "2.5.4" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? From d477de80c2865593c80bbd7f0a7ed0201d88aa0d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Oct 2024 10:44:46 -0700 Subject: [PATCH 251/333] Remove ios 16 conditions --- Localizable.xcstrings | 3 - Meshtastic/Helpers/BLEManager.swift | 82 ++--- Meshtastic/MeshtasticApp.swift | 31 +- Meshtastic/MeshtasticAppDelegate.swift | 28 +- Meshtastic/Views/Bluetooth/Connect.swift | 14 +- Meshtastic/Views/ContentView.swift | 21 +- .../Views/MapKitMap/NodeMapMapkit.swift | 322 +++++++++--------- Meshtastic/Views/Messages/MessageText.swift | 25 +- Meshtastic/Views/Messages/Messages.swift | 4 +- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 165 +++------ .../Views/Nodes/EnvironmentMetricsLog.swift | 6 +- .../Map/MapContent/MeshMapContent.swift | 1 - .../Nodes/Helpers/Map/PositionPopover.swift | 20 +- .../Views/Nodes/Helpers/NodeDetail.swift | 26 +- .../Views/Nodes/Helpers/NodeListItem.swift | 47 +-- Meshtastic/Views/Nodes/NodeList.swift | 12 +- Meshtastic/Views/Nodes/PaxCounterLog.swift | 6 +- Meshtastic/Views/Nodes/PositionLog.swift | 6 +- Meshtastic/Views/Settings/AppData.swift | 4 +- Meshtastic/Views/Settings/Channels.swift | 4 +- .../Settings/Config/Module/MQTTConfig.swift | 82 +++-- Meshtastic/Views/Settings/Settings.swift | 69 ++-- Meshtastic/Views/Settings/ShareChannels.swift | 6 +- 23 files changed, 386 insertions(+), 598 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2438475a..687024fb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -11481,9 +11481,6 @@ }, "Map Tile Data" : { - }, - "Map Type" : { - }, "map.centering" : { "extractionState" : "manual", diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 865f8d23..50e13f28 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1202,65 +1202,37 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @MainActor public func getPositionFromPhoneGPS(destNum: Int64, fixedPosition: Bool) -> Position? { var positionPacket = Position() - if #available(iOS 17.0, macOS 14.0, *) { - guard let lastLocation = LocationsHandler.shared.locationsArray.last else { - return nil - } + guard let lastLocation = LocationsHandler.shared.locationsArray.last else { + return nil + } - if lastLocation == CLLocation(latitude: 0, longitude: 0) { - return nil - } - - positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) - positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) - let timestamp = lastLocation.timestamp - positionPacket.time = UInt32(timestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(lastLocation.altitude) - positionPacket.satsInView = UInt32(LocationsHandler.satsInView) - let currentSpeed = lastLocation.speed - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(currentSpeed) - } - let currentHeading = lastLocation.course - if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(currentHeading) - } - /// Set location source for time - if !fixedPosition { - /// From GPS treat time as good - positionPacket.locationSource = Position.LocSource.locExternal - } else { - /// From GPS, but time can be old and have drifted - positionPacket.locationSource = Position.LocSource.locManual - } + if lastLocation == CLLocation(latitude: 0, longitude: 0) { + return nil + } + positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) + positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) + let timestamp = lastLocation.timestamp + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(lastLocation.altitude) + positionPacket.satsInView = UInt32(LocationsHandler.satsInView) + let currentSpeed = lastLocation.speed + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed) + } + let currentHeading = lastLocation.course + if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) + } + /// Set location source for time + if !fixedPosition { + /// From GPS treat time as good + positionPacket.locationSource = Position.LocSource.locExternal } else { - - positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) - positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) - let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() - positionPacket.time = UInt32(timestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) - positionPacket.satsInView = UInt32(LocationHelper.satsInView) - let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(currentSpeed) - } - let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 - if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(currentHeading) - } - /// Set location source for time - if !fixedPosition { - /// From GPS treat time as good - positionPacket.locationSource = Position.LocSource.locExternal - } else { - /// From GPS, but time can be old and have drifted - positionPacket.locationSource = Position.LocSource.locManual - } + /// From GPS, but time can be old and have drifted + positionPacket.locationSource = Position.LocSource.locManual } return positionPacket } diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 047a8cc9..2c5a55ee 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -5,7 +5,6 @@ import CoreData import OSLog import TipKit -@available(iOS 17.0, *) @main struct MeshtasticAppleApp: App { @@ -109,23 +108,21 @@ struct MeshtasticAppleApp: App { } }) .task { - if #available(iOS 17.0, macOS 14.0, *) { - #if DEBUG - /// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user. - /// This is for testing only, and should not be enabled in release builds. - try? Tips.resetDatastore() - #endif + #if DEBUG + /// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user. + /// This is for testing only, and should not be enabled in release builds. + try? Tips.resetDatastore() + #endif - try? Tips.configure( - [ - // Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project - .datastoreLocation(.applicationDefault), - // When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears. - // You can adjust this on per tip level as well - .displayFrequency(.immediate) - ] - ) - } + try? Tips.configure( + [ + // Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project + .datastoreLocation(.applicationDefault), + // When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears. + // You can adjust this on per tip level as well + .displayFrequency(.immediate) + ] + ) } } .onChange(of: scenePhase) { (newScenePhase) in diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index d38557bc..87226052 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -9,9 +9,9 @@ import SwiftUI import OSLog class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { - + var router: Router? - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Logger.services.info("🚀 [App] Meshtstic Apple App launched!") // Default User Default Values @@ -19,14 +19,11 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory": true]) UserDefaults.standard.register(defaults: ["meshMapShowRouteLines": true]) UNUserNotificationCenter.current().delegate = self - if #available(iOS 17.0, macOS 14.0, *) { - let locationsHandler = LocationsHandler.shared - locationsHandler.startLocationUpdates() - - // If a background activity session was previously active, reinstantiate it after the background launch. - if locationsHandler.backgroundActivity { - locationsHandler.backgroundActivity = true - } + let locationsHandler = LocationsHandler.shared + locationsHandler.startLocationUpdates() + // If a background activity session was previously active, reinstantiate it after the background launch. + if locationsHandler.backgroundActivity { + locationsHandler.backgroundActivity = true } return true } @@ -38,7 +35,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat ) { completionHandler([.list, .banner, .sound]) } - + // This method is called when a user clicks on the notification func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -46,11 +43,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat withCompletionHandler completionHandler: @escaping () -> Void ) { let userInfo = response.notification.request.content.userInfo - + switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier: break - case "messageNotification.thumbsUpAction": if let channel = userInfo["channel"] as? Int32, let replyID = userInfo["messageId"] as? Int64 { @@ -66,7 +62,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat Logger.services.error("Failed to retrieve channel or messageId from userInfo") } break - case "messageNotification.thumbsDownAction": if let channel = userInfo["channel"] as? Int32, let replyID = userInfo["messageId"] as? Int64 { @@ -82,7 +77,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat Logger.services.error("Failed to retrieve channel or messageId from userInfo") } break - case "messageNotification.replyInputAction": if let userInput = (response as? UNTextInputNotificationResponse)?.userText, let channel = userInfo["channel"] as? Int32, @@ -99,11 +93,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo") } break - default: break } - + if let targetValue = userInfo["target"] as? String, let deepLink = userInfo["path"] as? String, let url = URL(string: deepLink) { @@ -112,7 +105,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to handle notification response: \(userInfo)") } - completionHandler() } } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 90ec1de9..5bda12df 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -47,9 +47,7 @@ struct Connect: View { if bleManager.isSwitchedOn { Section(header: Text("connected.radio").font(.title)) { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { - if #available(iOS 17.0, macOS 14.0, *) { - TipView(BluetoothConnectionTip(), arrowEdge: .bottom) - } + TipView(BluetoothConnectionTip(), arrowEdge: .bottom) VStack(alignment: .leading) { HStack { VStack(alignment: .center) { @@ -76,12 +74,10 @@ struct Connect: View { .foregroundColor(.green) } else { HStack { - if #available(iOS 17.0, macOS 14.0, *) { - Image(systemName: "square.stack.3d.down.forward") - .symbolRenderingMode(.multicolor) - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) - .foregroundColor(.orange) - } + Image(systemName: "square.stack.3d.down.forward") + .symbolRenderingMode(.multicolor) + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .foregroundColor(.orange) Text("communicating").font(.callout) .foregroundColor(.orange) } diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 23b25f0c..cb5032ee 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -4,7 +4,6 @@ import SwiftUI -@available(iOS 17.0, *) struct ContentView: View { @ObservedObject var appState: AppState @@ -39,20 +38,12 @@ struct ContentView: View { } .tag(NavigationState.Tab.nodes) - if #available(iOS 17.0, macOS 14.0, *), !UserDefaults.mapUseLegacy { - MeshMap(router: appState.router) - .tabItem { - Label("map", systemImage: "map") - } - .tag(NavigationState.Tab.map) - } else { - NodeMap(router: appState.router) - .tabItem { - Label("map", systemImage: "map") - } - .tag(NavigationState.Tab.map) - } - + MeshMap(router: appState.router) + .tabItem { + Label("map", systemImage: "map") + } + .tag(NavigationState.Tab.map) + Settings( router: appState.router ) diff --git a/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift b/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift index 59c0e9cd..f9593585 100644 --- a/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift +++ b/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift @@ -1,164 +1,164 @@ +//// +//// NodeMapControl.swift +//// Meshtastic +//// +//// Created by Garth Vander Houwen on 9/9/23. +//// +//import SwiftUI +//import CoreLocation +//import MapKit +//import WeatherKit +//import OSLog // -// NodeMapControl.swift -// Meshtastic +//struct NodeMapMapkit: View { // -// Created by Garth Vander Houwen on 9/9/23. +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// /// Weather +// /// The current weather condition for the city. +// @State private var condition: WeatherCondition? +// @State private var temperature: Measurement? +// @State private var humidity: Int? +// @State private var symbolName: String = "cloud.fill" +// @State private var attributionLink: URL? +// @State private var attributionLogo: URL? // -import SwiftUI -import CoreLocation -import MapKit -import WeatherKit -import OSLog - -struct NodeMapMapkit: View { - - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - /// Weather - /// The current weather condition for the city. - @State private var condition: WeatherCondition? - @State private var temperature: Measurement? - @State private var humidity: Int? - @State private var symbolName: String = "cloud.fill" - @State private var attributionLink: URL? - @State private var attributionLogo: URL? - - @Environment(\.colorScheme) var colorScheme: ColorScheme - @AppStorage("meshMapType") private var meshMapType = 0 - @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false - @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false - @State private var selectedMapLayer: MapLayer = .standard - @State var waypointCoordinate: WaypointCoordinate? - @State var editingWaypoint: Int = 0 - @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( - mapName: "offlinemap", - tileType: "png", - canReplaceMapContent: true - ) - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - predicate: NSPredicate( - format: "expire == nil || expire >= %@", Date() as NSDate - ), animation: .none) - private var waypoints: FetchedResults - @ObservedObject var node: NodeInfoEntity - - var body: some View { - - NavigationStack { - GeometryReader { bounds in - VStack { - if node.hasPositions { - ZStack { - let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let lastTenThousand = Array(positionArray.prefix(10000)) - // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } - ZStack { - MapViewSwiftUI(onLongPress: { coord in - waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) - }, onWaypointEdit: { wpId in - if wpId > 0 { - waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) - } - }, - selectedMapLayer: selectedMapLayer, - positions: lastTenThousand, - waypoints: Array(waypoints), - userTrackingMode: MKUserTrackingMode.none, - showNodeHistory: meshMapShowNodeHistory, - showRouteLines: meshMapShowRouteLines, - customMapOverlay: self.customMapOverlay - ) - VStack(alignment: .leading) { - Spacer() - HStack(alignment: .bottom, spacing: 1) { - Picker("Map Type", selection: $selectedMapLayer) { - ForEach(MapLayer.allCases, id: \.self) { layer in - if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { - Text(layer.localized) - } else if layer != MapLayer.offline { - Text(layer.localized) - } - } - } - .onChange(of: (selectedMapLayer)) { newMapLayer in - UserDefaults.mapLayer = newMapLayer - } - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .pickerStyle(.menu) - .padding(5) - VStack { - VStack { - Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) - .font(.caption) - - Label("\(humidity ?? 0)%", systemImage: "humidity") - .font(.caption2) - - AsyncImage(url: attributionLogo) { image in - image - .resizable() - .scaledToFit() - } placeholder: { - ProgressView() - .controlSize(.mini) - } - .frame(height: 10) - - Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) - .font(.caption2) - } - .padding(5) - - } - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .padding(5) - .task { - do { - if node.hasPositions { - let mostRecent = node.positions?.lastObject as? PositionEntity - let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)) - condition = weather.currentWeather.condition - temperature = weather.currentWeather.temperature - humidity = Int(weather.currentWeather.humidity * 100) - symbolName = weather.currentWeather.symbolName - let attribution = try await WeatherService.shared.attribution - attributionLink = attribution.legalPageURL - attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL - } - } catch { - Logger.services.error("Could not gather weather information: \(error.localizedDescription)") - condition = .clear - symbolName = "cloud.fill" - } - } - } - } - } - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) - .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65) - } - } else { - HStack { - } - .padding([.top], 20) - } - } - .edgesIgnoringSafeArea([.leading, .trailing]) - .sheet(item: $waypointCoordinate, content: { wpc in - WaypointFormMapKit(coordinate: wpc) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - }) - .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) - .navigationBarItems(trailing: - ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) - } - .padding(.bottom, 2) - } - } -} +// @Environment(\.colorScheme) var colorScheme: ColorScheme +// @AppStorage("meshMapType") private var meshMapType = 0 +// @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false +// @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false +// @State private var selectedMapLayer: MapLayer = .standard +// @State var waypointCoordinate: WaypointCoordinate? +// @State var editingWaypoint: Int = 0 +// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( +// mapName: "offlinemap", +// tileType: "png", +// canReplaceMapContent: true +// ) +// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], +// predicate: NSPredicate( +// format: "expire == nil || expire >= %@", Date() as NSDate +// ), animation: .none) +// private var waypoints: FetchedResults +// @ObservedObject var node: NodeInfoEntity +// +// var body: some View { +// +// NavigationStack { +// GeometryReader { bounds in +// VStack { +// if node.hasPositions { +// ZStack { +// let positionArray = node.positions?.array as? [PositionEntity] ?? [] +// let lastTenThousand = Array(positionArray.prefix(10000)) +// // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } +// ZStack { +// MapViewSwiftUI(onLongPress: { coord in +// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) +// }, onWaypointEdit: { wpId in +// if wpId > 0 { +// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) +// } +// }, +// selectedMapLayer: selectedMapLayer, +// positions: lastTenThousand, +// waypoints: Array(waypoints), +// userTrackingMode: MKUserTrackingMode.none, +// showNodeHistory: meshMapShowNodeHistory, +// showRouteLines: meshMapShowRouteLines, +// customMapOverlay: self.customMapOverlay +// ) +// VStack(alignment: .leading) { +// Spacer() +// HStack(alignment: .bottom, spacing: 1) { +// Picker("Map Type", selection: $selectedMapLayer) { +// ForEach(MapLayer.allCases, id: \.self) { layer in +// if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { +// Text(layer.localized) +// } else if layer != MapLayer.offline { +// Text(layer.localized) +// } +// } +// } +// .onChange(of: (selectedMapLayer)) { newMapLayer in +// UserDefaults.mapLayer = newMapLayer +// } +// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) +// .pickerStyle(.menu) +// .padding(5) +// VStack { +// VStack { +// Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) +// .font(.caption) +// +// Label("\(humidity ?? 0)%", systemImage: "humidity") +// .font(.caption2) +// +// AsyncImage(url: attributionLogo) { image in +// image +// .resizable() +// .scaledToFit() +// } placeholder: { +// ProgressView() +// .controlSize(.mini) +// } +// .frame(height: 10) +// +// Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) +// .font(.caption2) +// } +// .padding(5) +// +// } +// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) +// .padding(5) +// .task { +// do { +// if node.hasPositions { +// let mostRecent = node.positions?.lastObject as? PositionEntity +// let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)) +// condition = weather.currentWeather.condition +// temperature = weather.currentWeather.temperature +// humidity = Int(weather.currentWeather.humidity * 100) +// symbolName = weather.currentWeather.symbolName +// let attribution = try await WeatherService.shared.attribution +// attributionLink = attribution.legalPageURL +// attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL +// } +// } catch { +// Logger.services.error("Could not gather weather information: \(error.localizedDescription)") +// condition = .clear +// symbolName = "cloud.fill" +// } +// } +// } +// } +// } +// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) +// .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65) +// } +// } else { +// HStack { +// } +// .padding([.top], 20) +// } +// } +// .edgesIgnoringSafeArea([.leading, .trailing]) +// .sheet(item: $waypointCoordinate, content: { wpc in +// WaypointFormMapKit(coordinate: wpc) +// .presentationDetents([.medium, .large]) +// .presentationDragIndicator(.automatic) +// }) +// .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) +// .navigationBarItems(trailing: +// ZStack { +// ConnectedDevice( +// bluetoothOn: bleManager.isSwitchedOn, +// deviceConnected: bleManager.connectedPeripheral != nil, +// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") +// }) +// } +// .padding(.bottom, 2) +// } +// } +//} diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 411511fd..df5b1f3d 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -47,23 +47,14 @@ struct MessageText: View { let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) if tapBackDestination.overlaySensorMessage { VStack { - if #available(iOS 17.0, macOS 14.0, *) { - isDetectionSensorMessage ? Image(systemName: "sensor.fill") - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .foregroundStyle(Color.orange) - .symbolRenderingMode(.multicolor) - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) - .offset(x: 20, y: -20) - : nil - } else { - isDetectionSensorMessage ? Image(systemName: "sensor.fill") - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .foregroundStyle(Color.orange) - .offset(x: 20, y: -20) - : nil - } + isDetectionSensorMessage ? Image(systemName: "sensor.fill") + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + .foregroundStyle(Color.orange) + .symbolRenderingMode(.multicolor) + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .offset(x: 20, y: -20) + : nil } } else { EmptyView() diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 4b347ca0..9785c7bf 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -63,9 +63,7 @@ struct Messages: View { } } - if #available(iOS 17.0, macOS 14.0, *) { - TipView(MessagesTip(), arrowEdge: .top) - } + TipView(MessagesTip(), arrowEdge: .top) } .navigationTitle("messages") .navigationBarTitleDisplayMode(.large) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 63e17424..9f30efc4 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -36,32 +36,31 @@ struct DeviceMetricsLog: View { .sorted { $0.time! < $1.time! } if chartData.count > 0 { GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { - if #available(iOS 17.0, macOS 14.0, *) { - Chart { - ForEach(chartData, id: \.self) { point in - Plot { - LineMark( - x: .value("x", point.time!), - y: .value("y", point.batteryLevel) - ) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") - .foregroundStyle(batteryChartColor) - .interpolationMethod(.linear) - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.channelUtilization) - ) - .symbolSize(25) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") - .foregroundStyle(channelUtilizationChartColor) - if let chartSelection { - RuleMark(x: .value("Second", chartSelection, unit: .second)) - .foregroundStyle(.tertiary.opacity(0.5)) + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + LineMark( + x: .value("x", point.time!), + y: .value("y", point.batteryLevel) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") + .foregroundStyle(batteryChartColor) + .interpolationMethod(.linear) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.channelUtilization) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") + .foregroundStyle(channelUtilizationChartColor) + if let chartSelection { + RuleMark(x: .value("Second", chartSelection, unit: .second)) + .foregroundStyle(.tertiary.opacity(0.5)) // .annotation( // position: .automatic, // overflowResolution: .init(x: .fit, y: .disabled) @@ -75,91 +74,37 @@ struct DeviceMetricsLog: View { // .foregroundStyle(Color.accentColor.opacity(0.2)) // } // } - } - RuleMark(y: .value("Network Status Orange", 25)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(.orange) - RuleMark(y: .value("Network Status Red", 50)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(.red) - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.airUtilTx) - ) - .symbolSize(25) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") - .foregroundStyle(airtimeChartColor) } - } - .chartXAxis(content: { - AxisMarks(position: .top) - }) - .chartXAxis(.automatic) - .chartXSelection(value: $chartSelection) - .chartYScale(domain: 0...100) - .chartForegroundStyleScale([ - idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, - "Channel Utilization": channelUtilizationChartColor, - "Airtime": airtimeChartColor - ]) - .chartLegend(position: .automatic, alignment: .bottom) - } else { - // Fallback on earlier versions - Chart { - ForEach(chartData, id: \.self) { point in - Plot { - LineMark( - x: .value("x", point.time!), - y: .value("y", point.batteryLevel) - ) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") - .foregroundStyle(batteryChartColor) - .interpolationMethod(.linear) - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.channelUtilization) - ) - .symbolSize(25) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") - .foregroundStyle(channelUtilizationChartColor) - RuleMark(y: .value("Network Status Orange", 25)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(.orange) - RuleMark(y: .value("Network Status Red", 50)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) - .foregroundStyle(.red) - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.airUtilTx) - ) - .symbolSize(25) - } - .accessibilityLabel("Line Series") - .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") - .foregroundStyle(airtimeChartColor) + RuleMark(y: .value("Network Status Orange", 25)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.orange) + RuleMark(y: .value("Network Status Red", 50)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.red) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.airUtilTx) + ) + .symbolSize(25) } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") + .foregroundStyle(airtimeChartColor) } - .chartXAxis(content: { - AxisMarks(position: .top) - }) - .chartXAxis(.automatic) - .chartYScale(domain: 0...100) - .chartForegroundStyleScale([ - idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, - "Channel Utilization": channelUtilizationChartColor, - "Airtime": airtimeChartColor - ]) - .chartLegend(position: .automatic, alignment: .bottom) } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartXSelection(value: $chartSelection) + .chartYScale(domain: 0...100) + .chartForegroundStyleScale([ + idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, + "Channel Utilization": channelUtilizationChartColor, + "Airtime": airtimeChartColor + ]) + .chartLegend(position: .automatic, alignment: .bottom) } .frame(minHeight: 240) } @@ -269,11 +214,7 @@ struct DeviceMetricsLog: View { chartSelection = metrics.time } } else { - if #available (iOS 17, *) { - ContentUnavailableView("No Device Metrics", systemImage: "slash.circle") - } else { - Text("No Device Metrics") - } + ContentUnavailableView("No Device Metrics", systemImage: "slash.circle") } } .navigationTitle("device.metrics.log") diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 0cd35248..56b0c82c 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -193,11 +193,7 @@ struct EnvironmentMetricsLog: View { } } else { - if #available (iOS 17, *) { - ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle") - } else { - Text("No Environment Metrics") - } + ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle") } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index edfec3be..dd49484a 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -8,7 +8,6 @@ import SwiftUI import MapKit -@available(iOS 17.0, macOS 14.0, *) struct MeshMapContent: MapContent { /// Parameters diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 35406707..88f61d76 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -214,20 +214,12 @@ struct PositionPopover: View { .padding(.bottom) } if position.nodePosition?.hasDetectionSensorMetrics ?? false { - if #available(iOS 17.0, macOS 14.0, *) { - Image(systemName: "sensor.fill") - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) - .symbolRenderingMode(.hierarchical) - .foregroundColor(.accentColor) - .font(.largeTitle) - .padding(.bottom) - } else { - Image(systemName: "sensor.fill") - .symbolRenderingMode(.hierarchical) - .foregroundColor(.accentColor) - .font(.largeTitle) - .padding(.bottom) - } + Image(systemName: "sensor.fill") + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .symbolRenderingMode(.hierarchical) + .foregroundColor(.accentColor) + .font(.largeTitle) + .padding(.bottom) } BatteryGauge(node: position.nodePosition!) } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 11defc79..a9f7bcfe 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -221,11 +221,7 @@ struct NodeDetail: View { .disabled(!node.hasDeviceMetrics) NavigationLink { - if #available (iOS 17, macOS 14, *) { - NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num) - } else { - NodeMapMapkit(node: node) - } + NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num) } label: { Label { Text("Node Map") @@ -260,19 +256,17 @@ struct NodeDetail: View { } .disabled(!node.hasEnvironmentMetrics) - if #available(iOS 17.0, macOS 14.0, *) { - NavigationLink { - TraceRouteLog(node: node) - } label: { - Label { - Text("Trace Route Log") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.multicolor) - } + NavigationLink { + TraceRouteLog(node: node) + } label: { + Label { + Text("Trace Route Log") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.multicolor) } - .disabled(node.traceRoutes?.count ?? 0 == 0) } + .disabled(node.traceRoutes?.count ?? 0 == 0) NavigationLink { DetectionSensorLog(node: node) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 481666a1..66f58a07 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -101,36 +101,9 @@ struct NodeListItem: View { if node.positions?.count ?? 0 > 0 && connectedNode != node.num { HStack { if let lastPostion = node.positions?.lastObject as? PositionEntity { - if #available(iOS 17.0, macOS 14.0, *) { - if let currentLocation = LocationsHandler.shared.locationsArray.last { - let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude) - if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude { - let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) - let metersAway = nodeCoord.distance(from: myCoord) - Image(systemName: "lines.measurement.horizontal") - .font(.callout) - .symbolRenderingMode(.multicolor) - .frame(width: 30) - DistanceText(meters: metersAway) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) - let headingDegrees = Angle.degrees(trueBearing) - Image(systemName: "location.north") - .font(.callout) - .symbolRenderingMode(.multicolor) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) - Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - } - } - } else { - - let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) - if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { + if let currentLocation = LocationsHandler.shared.locationsArray.last { + let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude) + if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude { let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) let metersAway = nodeCoord.distance(from: myCoord) Image(systemName: "lines.measurement.horizontal") @@ -139,7 +112,7 @@ struct NodeListItem: View { .frame(width: 30) DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.secondary) + .foregroundColor(.gray) let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) let headingDegrees = Angle.degrees(trueBearing) Image(systemName: "location.north") @@ -211,13 +184,11 @@ struct NodeListItem: View { .font(.callout) .frame(width: 30) } - if #available(iOS 17.0, macOS 14.0, *) { - if node.hasTraceRoutes { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - .font(.callout) - .frame(width: 30) - } + if node.hasTraceRoutes { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30) } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index f81a34cf..65f194cf 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -274,18 +274,10 @@ struct NodeList: View { ) } } else { - if #available (iOS 17, *) { - ContentUnavailableView("select.node", systemImage: "flipphone") - } else { - Text("select.node") - } + ContentUnavailableView("select.node", systemImage: "flipphone") } } detail: { - if #available (iOS 17, *) { - ContentUnavailableView("", systemImage: "line.3.horizontal") - } else { - Text("Select something to view") - } + ContentUnavailableView("", systemImage: "line.3.horizontal") } .navigationSplitViewStyle(.balanced) .onChange(of: searchText) { _ in diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 6d764c63..bb03aa9b 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -196,11 +196,7 @@ struct PaxCounterLog: View { .padding(.trailing) } } else { - if #available (iOS 17, *) { - ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle") - } else { - Text("paxcounter.content.unavailable") - } + ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle") } } .navigationTitle("paxcounter.log") diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 43354830..3abcb791 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -166,11 +166,7 @@ struct PositionLog: View { ) } else { - if #available (iOS 17, *) { - ContentUnavailableView("No Positions", systemImage: "mappin.slash") - } else { - Text("No Positions") - } + ContentUnavailableView("No Positions", systemImage: "mappin.slash") } } .navigationTitle("Position Log \(node.positions?.count ?? 0) Points") diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index bed0a948..049c73cf 100644 --- a/Meshtastic/Views/Settings/AppData.swift +++ b/Meshtastic/Views/Settings/AppData.swift @@ -22,9 +22,7 @@ struct AppData: View { VStack { Section(header: Text("phone.gps")) { - if #available(iOS 17.0, macOS 14.0, *) { - GPSStatus() - } + GPSStatus() } Divider() Button(action: { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index fbc9f75b..1a28a6be 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -60,9 +60,7 @@ struct Channels: View { VStack { List { - if #available(iOS 17.0, macOS 14.0, *) { - TipView(CreateChannelsTip(), arrowEdge: .bottom) - } + TipView(CreateChannelsTip(), arrowEdge: .bottom) if node != nil && node?.myInfo != nil { ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in Button(action: { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 6b39b649..c077c92e 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -353,52 +353,50 @@ struct MQTTConfig: View { } func setMqttValues() { - if #available(iOS 17.0, macOS 14.0, *) { + nearbyTopics = [] + let geocoder = CLGeocoder() + if LocationsHandler.shared.locationsArray.count > 0 { + let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic + defaultTopic = "msh/" + (region ?? "UNSET") + geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in + if let error { + Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)") + return + } - nearbyTopics = [] - let geocoder = CLGeocoder() - if LocationsHandler.shared.locationsArray.count > 0 { - let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic - defaultTopic = "msh/" + (region ?? "UNSET") - geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in - if let error { - Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)") - return + if let placemarks = placemarks, let placemark = placemarks.first { + let cc = locale.region?.identifier ?? "UNK" + /// Country Topic unless you are US + if placemark.isoCountryCode ?? "unknown" != cc { + let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "") + if !countryTopic.isEmpty { + nearbyTopics.append(countryTopic) + } } - - if let placemarks = placemarks, let placemark = placemarks.first { - let cc = locale.region?.identifier ?? "UNK" - /// Country Topic unless you are US - if placemark.isoCountryCode ?? "unknown" != cc { - let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "") - if !countryTopic.isEmpty { - nearbyTopics.append(countryTopic) - } - } - let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") - if !stateTopic.isEmpty { - nearbyTopics.append(stateTopic) - } - let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") - if !countyTopic.isEmpty { - nearbyTopics.append(countyTopic) - } - let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") - if !cityTopic.isEmpty { - nearbyTopics.append(cityTopic) - } - let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased() - .replacingOccurrences(of: " ", with: "") - .replacingOccurrences(of: "'", with: "") ?? "") - if !neightborhoodTopic.isEmpty { - nearbyTopics.append(neightborhoodTopic) - } - } else { - Logger.services.debug("No Location") + let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + if !stateTopic.isEmpty { + nearbyTopics.append(stateTopic) } - }) - } + let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + if !countyTopic.isEmpty { + nearbyTopics.append(countyTopic) + } + let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + if !cityTopic.isEmpty { + nearbyTopics.append(cityTopic) + } + let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased() + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "'", with: "") ?? "") + if !neightborhoodTopic.isEmpty { + nearbyTopics.append(neightborhoodTopic) + } + } else { + Logger.services.debug("No Location") + } + }) } + self.enabled = node?.mqttConfig?.enabled ?? false self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false self.address = node?.mqttConfig?.address ?? "" diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 6176512f..52a78b0a 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -154,16 +154,14 @@ struct Settings: View { var moduleConfigurationSection: some View { Section("module.configuration") { - if #available(iOS 17.0, macOS 14.0, *) { - NavigationLink(value: SettingsNavigationState.ambientLighting) { - Label { - Text("ambient.lighting") - } icon: { - Image(systemName: "light.max") - } + NavigationLink(value: SettingsNavigationState.ambientLighting) { + Label { + Text("ambient.lighting") + } icon: { + Image(systemName: "light.max") } } - + NavigationLink(value: SettingsNavigationState.cannedMessages) { Label { Text("canned.messages") @@ -321,25 +319,24 @@ struct Settings: View { Image(systemName: "gearshape") } } - if #available(iOS 17.0, macOS 14.0, *) { - NavigationLink(value: SettingsNavigationState.routes) { - Label { - Text("routes") - } icon: { - Image(systemName: "road.lanes.curved.right") - } - } - - NavigationLink(value: SettingsNavigationState.routeRecorder) { - Label { - Text("route.recorder") - } icon: { - Image(systemName: "record.circle") - .foregroundColor(.red) - } + NavigationLink(value: SettingsNavigationState.routes) { + Label { + Text("routes") + } icon: { + Image(systemName: "road.lanes.curved.right") } } + NavigationLink(value: SettingsNavigationState.routeRecorder) { + Label { + Text("route.recorder") + } icon: { + Image(systemName: "record.circle") + .foregroundColor(.red) + } + } + + if !(node?.deviceConfig?.isManaged ?? false) { if bleManager.connectedPeripheral != nil { Section("Configure") { @@ -403,9 +400,7 @@ struct Settings: View { } } } - if #available(iOS 17.0, macOS 14.0, *) { - TipView(AdminChannelTip(), arrowEdge: .top) - } + TipView(AdminChannelTip(), arrowEdge: .top) } else { if bleManager.connectedPeripheral != nil { Text("Connected Node \(node?.user?.longName ?? "unknown".localized)") @@ -416,9 +411,7 @@ struct Settings: View { radioConfigurationSection deviceConfigurationSection moduleConfigurationSection - if #available (iOS 17.0, *) { - loggingSection - } + loggingSection #if DEBUG developersSection #endif @@ -433,13 +426,9 @@ struct Settings: View { case .appSettings: AppSettings() case .routes: - if #available(iOS 17.0, *) { - Routes() - } + Routes() case .routeRecorder: - if #available(iOS 17.0, *) { - RouteRecorder() - } + RouteRecorder() case .lora: LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) case .channels: @@ -461,9 +450,7 @@ struct Settings: View { case .power: PowerConfig(node: nodes.first(where: { $0.num == selectedNode })) case .ambientLighting: - if #available(iOS 17.0, macOS 14.0, *) { - AmbientLightingConfig(node: node) - } + AmbientLightingConfig(node: node) case .cannedMessages: CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) case .detectionSensor: @@ -489,9 +476,7 @@ struct Settings: View { case .meshLog: MeshLog() case .debugLogs: - if #available(iOS 17.0, macOS 14.0, *) { - AppLog() - } + AppLog() case .appFiles: AppData() case .firmwareUpdates: diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index f4f4167a..e4b293a8 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -52,10 +52,8 @@ struct ShareChannels: View { var body: some View { - if #available(iOS 17.0, macOS 14.0, *) { - VStack { - TipView(ShareChannelsTip(), arrowEdge: .bottom) - } + VStack { + TipView(ShareChannelsTip(), arrowEdge: .bottom) } GeometryReader { bounds in let smallest = min(bounds.size.width, bounds.size.height) From 26e917df48d7cc9cd65935d0329c1aff15895d50 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Oct 2024 13:52:38 -0700 Subject: [PATCH 252/333] Remove serial --- Localizable.xcstrings | 3 -- .../contents | 1 + Meshtastic/Persistence/UpdateCoreData.swift | 4 +-- .../Views/Settings/Config/DeviceConfig.swift | 34 +++++++------------ 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 687024fb..e284ac42 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -18945,9 +18945,6 @@ }, "Select a Trace Route" : { - }, - "Select something to view" : { - }, "select.contact" : { "extractionState" : "manual", diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index 2a5e4a56..687237cb 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -69,6 +69,7 @@ + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index faa8e606..fc8eac8e 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -462,24 +462,24 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi if fetchedNode[0].deviceConfig == nil { let newDeviceConfig = DeviceConfigEntity(context: context) newDeviceConfig.role = Int32(config.role.rawValue) - newDeviceConfig.serialEnabled = config.serialEnabled newDeviceConfig.buttonGpio = Int32(config.buttonGpio) newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress + newDeviceConfig.tripleClickAsAdHocPing = !config.disableTripleClick newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled newDeviceConfig.isManaged = config.isManaged newDeviceConfig.tzdef = config.tzdef fetchedNode[0].deviceConfig = newDeviceConfig } else { fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) - fetchedNode[0].deviceConfig?.serialEnabled = config.serialEnabled fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress + fetchedNode[0].deviceConfig?.tripleClickAsAdHocPing = !config.disableTripleClick fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled fetchedNode[0].deviceConfig?.isManaged = config.isManaged fetchedNode[0].deviceConfig?.tzdef = config.tzdef diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index fd637659..117c0683 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -23,7 +23,6 @@ struct DeviceConfig: View { @State var deviceRole = 0 @State var buzzerGPIO = 0 @State var buttonGPIO = 0 - @State var serialEnabled = true @State var rebroadcastMode = 0 @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @@ -77,7 +76,7 @@ struct DeviceConfig: View { Text("Treat double tap on supported accelerometers as a user button press.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $tripleClickAsAdHocPing) { Label("Triple Click Ad Hoc Ping", systemImage: "map.pin") Text("Send a position on the primary channel when the user button is triple clicked.") @@ -91,10 +90,6 @@ struct DeviceConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("Debug")) { - Toggle(isOn: $serialEnabled) { - Label("Serial Console", systemImage: "terminal") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) VStack(alignment: .leading) { HStack { Label("Time Zone", systemImage: "clock.badge.exclamationmark") @@ -114,7 +109,7 @@ struct DeviceConfig: View { .disableAutocorrection(true) Text("Time zone for dates on the device screen and log.") .foregroundColor(.gray) - .font(.callout) + .font(.callout) } } Section(header: Text("GPIO")) { @@ -201,12 +196,12 @@ struct DeviceConfig: View { if connectedNode != nil { var dc = Config.DeviceConfig() dc.role = DeviceRoles(rawValue: deviceRole)!.protoEnumValue() - dc.serialEnabled = serialEnabled dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress + dc.disableTripleClick = !tripleClickAsAdHocPing dc.tzdef = tzdef dc.ledHeartbeatDisabled = !ledHeartbeatEnabled let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) @@ -254,20 +249,17 @@ struct DeviceConfig: View { } } } - .onChange(of: deviceRole) { - if $0 != node?.deviceConfig?.role ?? -1 { hasChanges = true } + .onChange(of: deviceRole) { oldRole, newRole in + if oldRole != newRole && newRole != node?.deviceConfig?.role ?? -1 { hasChanges = true } } - .onChange(of: serialEnabled) { - if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true } + .onChange(of: buttonGPIO) { oldButtonGPIO, newButtonGPIO in + if oldButtonGPIO != newButtonGPIO && newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } - .onChange(of: buttonGPIO) { newButtonGPIO in - if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } + .onChange(of: buzzerGPIO) { oldBuzzerGPIO, newBuzzerGPIO in + if oldBuzzerGPIO != newBuzzerGPIO && newBuzzerGPIO != node?.deviceConfig?.buzzerGpio ?? -1 { hasChanges = true } } - .onChange(of: buzzerGPIO) { newBuzzerGPIO in - if newBuzzerGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } - } - .onChange(of: rebroadcastMode) { newRebroadcastMode in - if newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true } + .onChange(of: rebroadcastMode) { oldRebroadcastMode, newRebroadcastMode in + if oldRebroadcastMode != newRebroadcastMode && newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true } } .onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in if newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true } @@ -276,7 +268,7 @@ struct DeviceConfig: View { if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } } .onChange(of: tripleClickAsAdHocPing) { - // if $0 != node?.deviceConfig?.tripleClickAsAdHocPing { hasChanges = true } + if $0 != node?.deviceConfig?.tripleClickAsAdHocPing { hasChanges = true } } .onChange(of: tzdef) { newTzdef in if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } @@ -289,7 +281,6 @@ struct DeviceConfig: View { } func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) - self.serialEnabled = (node?.deviceConfig?.serialEnabled ?? true) self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0) self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) @@ -298,6 +289,7 @@ struct DeviceConfig: View { nodeInfoBroadcastSecs = 3600 } self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false + self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? false self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true self.tzdef = node?.deviceConfig?.tzdef ?? "" if self.tzdef.isEmpty { From a4d5aefca376aa377917f3c2263b9c5bf13052a7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Oct 2024 15:50:57 -0700 Subject: [PATCH 253/333] Bossy linter --- Meshtastic/AppIntents/AppIntentErrors.swift | 2 +- .../AppIntents/FactoryResetNodeIntent.swift | 6 +- .../AppIntents/MessageChannelIntent.swift | 2 +- .../AppIntents/NodePositionIntent.swift | 62 +++++++++---------- Meshtastic/Enums/TelemetryEnums.swift | 1 - Meshtastic/Helpers/BLEManager.swift | 56 +++++++---------- .../Helpers/LocalNotificationManager.swift | 18 ++---- Meshtastic/Helpers/MeshPackets.swift | 14 ++--- Meshtastic/MeshtasticApp.swift | 2 +- Meshtastic/MeshtasticAppDelegate.swift | 3 - Meshtastic/Persistence/UpdateCoreData.swift | 8 +-- Meshtastic/Views/Bluetooth/Connect.swift | 4 +- Meshtastic/Views/ContentView.swift | 2 +- .../Views/MapKitMap/WaypointFormMapKit.swift | 4 +- .../Views/Messages/ChannelMessageList.swift | 4 +- Meshtastic/Views/Messages/Messages.swift | 2 +- .../TextMessageField/TextMessageField.swift | 4 +- Meshtastic/Views/Messages/UserList.swift | 16 ++--- .../Views/Messages/UserMessageList.swift | 4 +- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 2 +- .../Nodes/Helpers/Map/MapSettingsForm.swift | 4 +- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 +- .../Nodes/Helpers/Map/PositionPopover.swift | 1 - Meshtastic/Views/Nodes/NodeList.swift | 18 +++--- Meshtastic/Views/Settings/AppData.swift | 2 - Meshtastic/Views/Settings/AppLog.swift | 8 +-- .../Views/Settings/Channels/ChannelForm.swift | 30 ++++----- .../Settings/Config/BluetoothConfig.swift | 4 +- .../Views/Settings/Config/DeviceConfig.swift | 2 +- .../Config/Module/CannedMessagesConfig.swift | 5 +- .../Settings/Config/Module/MQTTConfig.swift | 16 ++--- .../Config/Module/PaxCounterConfig.swift | 8 +-- .../Config/Module/StoreForwardConfig.swift | 24 +++---- .../Views/Settings/Config/NetworkConfig.swift | 12 ++-- .../Settings/Config/PositionConfig.swift | 2 +- .../Views/Settings/Config/PowerConfig.swift | 4 +- .../Settings/Config/SecurityConfig.swift | 6 +- Meshtastic/Views/Settings/RouteRecorder.swift | 2 +- Meshtastic/Views/Settings/Routes.swift | 13 ++-- Meshtastic/Views/Settings/Settings.swift | 7 +-- Meshtastic/Views/Settings/ShareChannels.swift | 18 +++--- Meshtastic/Views/Settings/UserConfig.swift | 18 +++--- Widgets/WidgetsLiveActivity.swift | 1 - 43 files changed, 191 insertions(+), 232 deletions(-) diff --git a/Meshtastic/AppIntents/AppIntentErrors.swift b/Meshtastic/AppIntents/AppIntentErrors.swift index 8e80b6b6..427c30ae 100644 --- a/Meshtastic/AppIntents/AppIntentErrors.swift +++ b/Meshtastic/AppIntents/AppIntentErrors.swift @@ -15,7 +15,7 @@ class AppIntentErrors { var localizedStringResource: LocalizedStringResource { switch self { - case let .message(message): + case let .message(message): Logger.services.error("App Intent: \(message)") return "Error: \(message)" case .notConnected: diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift index 7ff2bd92..60946b00 100644 --- a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -11,10 +11,10 @@ import AppIntents struct FactoryResetNodeIntent: AppIntent { static var title: LocalizedStringResource = "Factory Reset" static var description: IntentDescription = "Perform a factory reset on the node you are connected to" - + func perform() async throws -> some IntentResult { // Request user confirmation before performing the factory reset - try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"),confirmationActionName: ConfirmationActionName + try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) // Ensure the node is connected @@ -27,7 +27,7 @@ struct FactoryResetNodeIntent: AppIntent { let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), let fromUser = connectedNode.user, let toUser = connectedNode.user { - + // Attempt to send a factory reset command, throw an error if it fails if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) { throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset") diff --git a/Meshtastic/AppIntents/MessageChannelIntent.swift b/Meshtastic/AppIntents/MessageChannelIntent.swift index e387e17c..aa9ea47a 100644 --- a/Meshtastic/AppIntents/MessageChannelIntent.swift +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -36,7 +36,7 @@ struct MessageChannelIntent: AppIntent { guard let messageData = messageContent.data(using: .utf8) else { throw AppIntentErrors.AppIntentError.message("Failed to encode message content") } - + if messageData.count > 200 { throw $messageContent.needsValueError("Message content exceeds 200 bytes.") } diff --git a/Meshtastic/AppIntents/NodePositionIntent.swift b/Meshtastic/AppIntents/NodePositionIntent.swift index a173df0d..1e052eb9 100644 --- a/Meshtastic/AppIntents/NodePositionIntent.swift +++ b/Meshtastic/AppIntents/NodePositionIntent.swift @@ -14,44 +14,38 @@ struct NodePositionIntent: AppIntent { @Parameter(title: "Node Number") var nodeNum: Int - + static var title: LocalizedStringResource = "Get Node Position" static var description: IntentDescription = "Fetch the latest position of a cetain node" - - + func perform() async throws -> some IntentResult & ReturnsValue { - if (!BLEManager.shared.isConnected) { + if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected } - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - do { - guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else { - throw $nodeNum.needsValueError("Could not find node") - } - - let nodeInfo = fetchedNode[0] - nodeInfo.latestEnvironmentMetrics?.batteryLevel - if let latitude = nodeInfo.latestPosition?.coordinate.latitude, - let longitude = nodeInfo.latestPosition?.coordinate.longitude { - let nodeLocation = CLLocation(latitude: latitude, longitude: longitude) - - // Reverse geocode the CLLocation to get a CLPlacemark - let geocoder = CLGeocoder() - let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation) - - if let placemark = placemarks.first { - return .result(value: placemark) - } else { - throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location") - } - } else { - throw AppIntentErrors.AppIntentError.message("Node does not have positions") - } - } catch { - throw AppIntentErrors.AppIntentError.message("Fetch Failure") + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + do { + guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else { + throw $nodeNum.needsValueError("Could not find node") } - } - -} + let nodeInfo = fetchedNode[0] + if let latitude = nodeInfo.latestPosition?.coordinate.latitude, + let longitude = nodeInfo.latestPosition?.coordinate.longitude { + let nodeLocation = CLLocation(latitude: latitude, longitude: longitude) + // Reverse geocode the CLLocation to get a CLPlacemark + let geocoder = CLGeocoder() + let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation) + if let placemark = placemarks.first { + return .result(value: placemark) + } else { + throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location") + } + } else { + throw AppIntentErrors.AppIntentError.message("Node does not have positions") + } + } catch { + throw AppIntentErrors.AppIntentError.message("Fetch Failure") + } + } +} diff --git a/Meshtastic/Enums/TelemetryEnums.swift b/Meshtastic/Enums/TelemetryEnums.swift index 8de53403..213d6963 100644 --- a/Meshtastic/Enums/TelemetryEnums.swift +++ b/Meshtastic/Enums/TelemetryEnums.swift @@ -177,7 +177,6 @@ enum Iaq: Int, CaseIterable, Identifiable { } } - // Default of 0 is Client enum MetricsTypes: Int, CaseIterable, Identifiable { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 50e13f28..805f32a2 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -54,31 +54,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") // MARK: init - private override init() { - // Default initialization should not be used - fatalError("Use setup(appState:context:) to initialize the singleton") - } + // Default initialization should not be used + fatalError("Use setup(appState:context:) to initialize the singleton") + } - static func setup(appState: AppState, context: NSManagedObjectContext) { - guard shared == nil else { - print("BLEManager already initialized") - return - } - shared = BLEManager(appState: appState, context: context) + static func setup(appState: AppState, context: NSManagedObjectContext) { + guard shared == nil else { + Logger.services.warning("[BLE] BLEManager already initialized") + return } + shared = BLEManager(appState: appState, context: context) + } - private init(appState: AppState, context: NSManagedObjectContext) { - self.appState = appState - self.context = context - self.lastConnectionError = "" - self.connectedVersion = "0.0.0" - super.init() - centralManager = CBCentralManager(delegate: self, queue: nil) - mqttManager.delegate = self - } - - + private init(appState: AppState, context: NSManagedObjectContext) { + self.appState = appState + self.context = context + self.lastConnectionError = "" + self.connectedVersion = "0.0.0" + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + mqttManager.delegate = self + } // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID @@ -464,7 +461,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { let fetchedNodes = try context.fetch(nodes) let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) - let connectedNode = fetchedNodes.first(where: { $0.num == self.connectedPeripheral.num }) traceRoute.id = Int64(meshPacket.id) traceRoute.time = Date() traceRoute.node = receivingNode @@ -1617,7 +1613,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { - let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) + let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData) for cs in channelSet.settings { if addChannels { // We are trying to add a channel so lets get the last index @@ -1989,10 +1985,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey,context: context) + upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } - return 0 } @@ -3195,7 +3190,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { + if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) { // Handle each of the store and forward request / response messages switch storeAndForwardMessage.rr { case .unset: @@ -3321,7 +3316,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MARK: - CB Central Manager implmentation extension BLEManager: CBCentralManagerDelegate { - + // MARK: Bluetooth enabled/disabled func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == CBManagerState.poweredOn { @@ -3331,9 +3326,8 @@ extension BLEManager: CBCentralManagerDelegate { } else { isSwitchedOn = false } - + var status = "" - switch central.state { case .poweredOff: status = "BLE is powered off" @@ -3352,10 +3346,9 @@ extension BLEManager: CBCentralManagerDelegate { } Logger.services.info("📜 [BLE] Bluetooth status: \(status)") } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) Logger.services.info("✅ [BLE] Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown", privacy: .public)") @@ -3363,7 +3356,6 @@ extension BLEManager: CBCentralManagerDelegate { let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral) - if let peripheralIndex = index { peripherals[peripheralIndex] = device } else { diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 1260f20c..47f64bce 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -5,16 +5,9 @@ import OSLog class LocalNotificationManager { var notifications = [Notification]() - - let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: - "👍 \(Tapbacks.thumbsUp.description)", options: []) - let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: - "👎 \(Tapbacks.thumbsDown.description)", options: []) - let replyInputAction = UNTextInputNotificationAction( - identifier: "messageNotification.replyInputAction", - title: "reply".localized, - options: []) - + let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "👍 \(Tapbacks.thumbsUp.description)", options: []) + let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "👎 \(Tapbacks.thumbsDown.description)", options: []) + let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: []) // Step 1 Request Permissions for notifications private func requestAuthorization() { @@ -43,13 +36,13 @@ class LocalNotificationManager { private func scheduleNotifications() { let messageNotificationCategory = UNNotificationCategory( identifier: "messageNotificationCategory", - actions: [thumbsUpAction, thumbsDownAction,replyInputAction], + actions: [thumbsUpAction, thumbsDownAction, replyInputAction], intentIdentifiers: [], options: .customDismissAction ) UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) - + for notification in notifications { let content = UNMutableNotificationContent() content.subtitle = notification.subtitle @@ -75,7 +68,6 @@ class LocalNotificationManager { content.userInfo["userNum"] = notification.userNum } - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 7d0c6a6b..9c4ed69c 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -50,7 +50,6 @@ func generateMessageMarkdown (message: String) -> String { func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - let remote = nodeNum != UserDefaults.preferredPeripheralNum if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { @@ -454,11 +453,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) { + if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) { if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { - if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { + if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) { if !cmmc.messages.isEmpty { @@ -581,7 +580,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) { do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) - if let paxMessage = try? Paxcount(serializedData: packet.decoded.payload) { + if let paxMessage = try? Paxcount(serializedBytes: packet.decoded.payload) { let newPax = PaxCounterEntity(context: context) newPax.ble = Int32(truncatingIfNeeded: paxMessage.ble) @@ -611,7 +610,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) { func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - if let routingMessage = try? Routing(serializedData: packet.decoded.payload) { + if let routingMessage = try? Routing(serializedBytes: packet.decoded.payload) { let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue) @@ -833,7 +832,7 @@ func textMessageAppPacket( } var storeForwardBroadcast = false if storeForward { - if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { + if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) { messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8) if storeAndForwardMessage.rr == .routerTextBroadcast { storeForwardBroadcast = true @@ -993,7 +992,6 @@ func textMessageAppPacket( } } - func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) @@ -1004,7 +1002,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { do { - if let waypointMessage = try? Waypoint(serializedData: packet.decoded.payload) { + if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { let fetchedWaypoint = try context.fetch(fetchWaypointRequest) if fetchedWaypoint.isEmpty { let waypoint = WaypointEntity(context: context) diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 2c5a55ee..b7b28f35 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -125,7 +125,7 @@ struct MeshtasticAppleApp: App { ) } } - .onChange(of: scenePhase) { (newScenePhase) in + .onChange(of: scenePhase) { (_, newScenePhase) in switch newScenePhase { case .background: Logger.services.info("🎬 [App] Scene is in the background") diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 87226052..801fa955 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -61,7 +61,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to retrieve channel or messageId from userInfo") } - break case "messageNotification.thumbsDownAction": if let channel = userInfo["channel"] as? Int32, let replyID = userInfo["messageId"] as? Int64 { @@ -76,7 +75,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to retrieve channel or messageId from userInfo") } - break case "messageNotification.replyInputAction": if let userInput = (response as? UNTextInputNotificationResponse)?.userText, let channel = userInfo["channel"] as? Int32, @@ -92,7 +90,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo") } - break default: break } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index fc8eac8e..69757588 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -159,12 +159,12 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum { newNode.channel = Int32(packet.channel) } - if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { + if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) newNode.favorite = nodeInfoMessage.isFavorite } - if let newUserMessage = try? User(serializedData: packet.decoded.payload) { + if let newUserMessage = try? User(serializedBytes: packet.decoded.payload) { if newUserMessage.id.isEmpty { if packet.from > Constants.minimumNodeNum { @@ -254,7 +254,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].channel = Int32(packet.channel) } - if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { + if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) fetchedNode[0].favorite = nodeInfoMessage.isFavorite @@ -320,7 +320,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) do { - if let positionMessage = try? Position(serializedData: packet.decoded.payload) { + if let positionMessage = try? Position(serializedBytes: packet.decoded.payload) { /// Don't save empty position packets from null island or apple park if (positionMessage.longitudeI != 0 && positionMessage.latitudeI != 0) && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 5bda12df..c26b7676 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -301,10 +301,10 @@ struct Connect: View { .presentationDetents([.large]) .presentationDragIndicator(.automatic) } - .onChange(of: (self.bleManager.invalidVersion)) { _ in + .onChange(of: self.bleManager.invalidVersion) { invalidFirmwareVersion = self.bleManager.invalidVersion } - .onChange(of: (self.bleManager.isSubscribed)) { sub in + .onChange(of: self.bleManager.isSubscribed) { _, sub in if UserDefaults.preferredPeripheralId.count > 0 && sub { diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index cb5032ee..b122b0aa 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -43,7 +43,7 @@ struct ContentView: View { Label("map", systemImage: "map") } .tag(NavigationState.Tab.map) - + Settings( router: appState.router ) diff --git a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index 5fc5e2f4..f2451a28 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -51,7 +51,7 @@ struct WaypointFormMapKit: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: name, perform: { _ in + .onChange(of: name) { var totalBytes = name.utf8.count // Only mess with the value if it is too big while totalBytes > 30 { @@ -61,7 +61,7 @@ struct WaypointFormMapKit: View { if totalBytes > 30 { name = String(name.dropLast()) } - }) + } } HStack { Text("Description") diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 36cc5592..1b783427 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -134,11 +134,11 @@ struct ChannelMessageList: View { scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } } - .onChange(of: channel.allPrivateMessages, perform: { _ in + .onChange(of: channel.allPrivateMessages) { withAnimation { scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } - }) + } } TextMessageField( diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 9785c7bf..1267d3e2 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -87,7 +87,7 @@ struct Messages: View { } else if case .directMessages = router.navigationState.messages { Text("Select a conversation") } - }.onChange(of: router.navigationState) { _ in + }.onChange(of: router.navigationState) { setupNavigationState() } } diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index 7aded0f1..1eca5015 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -30,14 +30,14 @@ struct TextMessageField: View { HStack(alignment: .top) { ZStack { TextField("message", text: $typingMessage, axis: .vertical) - .onChange(of: typingMessage, perform: { value in + .onChange(of: typingMessage) { _, value in totalBytes = value.utf8.count // Only mess with the value if it is too big while totalBytes > Self.maxbytes { typingMessage = String(typingMessage.dropLast()) totalBytes = typingMessage.utf8.count } - }) + } .keyboardType(.default) .toolbar { ToolbarItemGroup(placement: .keyboard) { diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 9d25b590..4a4028e2 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -199,12 +199,12 @@ struct UserList: View { .sheet(isPresented: $showingHelp) { DirectMessagesHelp() } - .onChange(of: searchText) { _ in + .onChange(of: searchText) { Task { await searchUserList() } } - .onChange(of: viaLora) { _ in + .onChange(of: viaLora) { if !viaLora && !viaMqtt { viaMqtt = true } @@ -212,7 +212,7 @@ struct UserList: View { await searchUserList() } } - .onChange(of: viaMqtt) { _ in + .onChange(of: viaMqtt) { if !viaLora && !viaMqtt { viaLora = true } @@ -220,27 +220,27 @@ struct UserList: View { await searchUserList() } } - .onChange(of: [deviceRoles]) { _ in + .onChange(of: [deviceRoles]) { Task { await searchUserList() } } - .onChange(of: hopsAway) { _ in + .onChange(of: hopsAway) { Task { await searchUserList() } } - .onChange(of: [boolFilters]) { _ in + .onChange(of: [boolFilters]) { Task { await searchUserList() } } - .onChange(of: maxDistance) { _ in + .onChange(of: maxDistance) { Task { await searchUserList() } } - .onChange(of: isPkiEncrypted) { _ in + .onChange(of: isPkiEncrypted) { Task { await searchUserList() } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index cb483745..7ce190fc 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -122,11 +122,11 @@ struct UserMessageList: View { scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } } - .onChange(of: user.messageList, perform: { _ in + .onChange(of: user.messageList) { withAnimation { scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) } - }) + } } TextMessageField( diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 9f30efc4..0239e476 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -207,7 +207,7 @@ struct DeviceMetricsLog: View { .padding(.bottom) .padding(.trailing) } - .onChange(of: selection) { newSelection in + .onChange(of: selection) { _, newSelection in guard let metrics = deviceMetrics.first(where: { $0.id == newSelection }) else { return } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index fed672d0..088787b9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -36,7 +36,7 @@ struct MapSettingsForm: View { .pickerStyle(SegmentedPickerStyle()) .padding(.top, 5) .padding(.bottom, 5) - .onChange(of: mapLayer) { newMapLayer in + .onChange(of: mapLayer) { _, newMapLayer in UserDefaults.mapLayer = newMapLayer } if meshMap { @@ -50,7 +50,7 @@ struct MapSettingsForm: View { } .pickerStyle(DefaultPickerStyle()) } - .onChange(of: meshMapDistance) { newMeshMapDistance in + .onChange(of: meshMapDistance) { _, newMeshMapDistance in UserDefaults.meshMapDistance = newMeshMapDistance } Toggle(isOn: $waypoints) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 5b264867..1120e811 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -80,7 +80,7 @@ struct NodeMapSwiftUI: View { } .sheet(isPresented: $isEditingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) - .onChange(of: (selectedMapLayer)) { newMapLayer in + .onChange(of: (selectedMapLayer)) { _, newMapLayer in switch selectedMapLayer { case .standard: UserDefaults.mapLayer = newMapLayer diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 88f61d76..fb019e0b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -95,7 +95,6 @@ struct PositionPopover: View { } /// Altitude Label { - let formatter = MeasurementFormatter() let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters) let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet) if Locale.current.measurementSystem == .metric { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 65f194cf..756f1179 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -280,12 +280,12 @@ struct NodeList: View { ContentUnavailableView("", systemImage: "line.3.horizontal") } .navigationSplitViewStyle(.balanced) - .onChange(of: searchText) { _ in + .onChange(of: searchText) { Task { await searchNodeList() } } - .onChange(of: viaLora) { _ in + .onChange(of: viaLora) { if !viaLora && !viaMqtt { viaMqtt = true } @@ -293,7 +293,7 @@ struct NodeList: View { await searchNodeList() } } - .onChange(of: viaMqtt) { _ in + .onChange(of: viaMqtt) { if !viaLora && !viaMqtt { viaLora = true } @@ -301,32 +301,32 @@ struct NodeList: View { await searchNodeList() } } - .onChange(of: [boolFilters]) { _ in + .onChange(of: [boolFilters]) { Task { await searchNodeList() } } - .onChange(of: [deviceRoles]) { _ in + .onChange(of: [deviceRoles]) { Task { await searchNodeList() } } - .onChange(of: hopsAway) { _ in + .onChange(of: hopsAway) { Task { await searchNodeList() } } - .onChange(of: maxDistance) { _ in + .onChange(of: maxDistance) { Task { await searchNodeList() } } - .onChange(of: distanceFilter) { _ in + .onChange(of: distanceFilter) { Task { await searchNodeList() } } - .onChange(of: router.navigationState) { _ in + .onChange(of: router.navigationState) { if let selected = router.navigationState.nodeListSelectedNodeNum { self.selectedNode = getNodeInfo(id: selected, context: context) } else { diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index 049c73cf..3f1ebf0e 100644 --- a/Meshtastic/Views/Settings/AppData.swift +++ b/Meshtastic/Views/Settings/AppData.swift @@ -67,8 +67,6 @@ struct AppData: View { let container = NSPersistentContainer(name: "Meshtastic") do { try container.restorePersistentStore(from: file.absoluteURL) - let request = MyInfoEntity.fetchRequest() - try context.fetch(request) UserDefaults.preferredPeripheralId = "" UserDefaults.preferredPeripheralNum = Int(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10]) ?? 0 Logger.data.notice("🗂️ Restored a core data backup to backup/\(UserDefaults.preferredPeripheralNum, privacy: .public)") diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index e1b2e15c..b96ed806 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -131,25 +131,25 @@ struct AppLog: View { logs.sort(using: sortOrder) } } - .onChange(of: searchText) { _ in + .onChange(of: searchText) { Task { await logs = searchAppLogs() logs.sort(using: sortOrder) } } - .onChange(of: [categories]) { _ in + .onChange(of: [categories]) { Task { await logs = searchAppLogs() logs.sort(using: sortOrder) } } - .onChange(of: [levels]) { _ in + .onChange(of: [levels]) { Task { await logs = searchAppLogs() logs.sort(using: sortOrder) } } - .onChange(of: selection) { newSelection in + .onChange(of: selection) { _, newSelection in presentingErrorDetails = true let log = logs.first { $0.id == newSelection diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 05e8fd27..72d6f58b 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -39,7 +39,7 @@ struct ChannelForm: View { .disableAutocorrection(true) .keyboardType(.alphabet) .foregroundColor(Color.gray) - .onChange(of: channelName, perform: { _ in + .onChange(of: channelName) { channelName = channelName.replacing(" ", with: "") var totalBytes = channelName.utf8.count // Only mess with the value if it is too big @@ -48,7 +48,7 @@ struct ChannelForm: View { totalBytes = channelName.utf8.count } hasChanges = true - }) + } } HStack { Picker("Key Size", selection: $channelKeySize) { @@ -97,7 +97,7 @@ struct ChannelForm: View { , lineWidth: 2.0) ) - .onChange(of: channelKey, perform: { _ in + .onChange(of: channelKey) { let tempKey = Data(base64Encoded: channelKey) ?? Data() if tempKey.count == channelKeySize || channelKeySize == -1 { @@ -106,7 +106,7 @@ struct ChannelForm: View { hasValidKey = false } hasChanges = true - }) + } .disabled(channelKeySize <= 0) } HStack { @@ -146,7 +146,7 @@ struct ChannelForm: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .disabled(!supportedVersion) .listRowSeparator(.visible) - .onChange(of: preciseLocation) { pl in + .onChange(of: preciseLocation) { _, pl in if pl == false { positionPrecision = 14 } @@ -184,10 +184,10 @@ struct ChannelForm: View { .listRowSeparator(.visible) } } - .onChange(of: channelName) { _ in + .onChange(of: channelName) { hasChanges = true } - .onChange(of: channelKeySize) { _ in + .onChange(of: channelKeySize) { if channelKeySize == -1 { channelKey = "AQ==" } else { @@ -196,10 +196,10 @@ struct ChannelForm: View { } hasChanges = true } - .onChange(of: channelKey) { _ in + .onChange(of: channelKey) { hasChanges = true } - .onChange(of: channelKeySize) { _ in + .onChange(of: channelKeySize) { if channelKeySize == -1 { if channelRole == 0 { preciseLocation = false @@ -207,10 +207,10 @@ struct ChannelForm: View { channelKey = "AQ==" } } - .onChange(of: channelRole) { _ in + .onChange(of: channelRole) { hasChanges = true } - .onChange(of: preciseLocation) { loc in + .onChange(of: preciseLocation) { _, loc in if loc == true { if channelKey == "AQ==" { preciseLocation = false @@ -223,10 +223,10 @@ struct ChannelForm: View { } hasChanges = true } - .onChange(of: positionPrecision) { _ in + .onChange(of: positionPrecision) { hasChanges = true } - .onChange(of: positionsEnabled) { pe in + .onChange(of: positionsEnabled) { _, pe in if pe { if positionPrecision == 0 { positionPrecision = 14 @@ -236,10 +236,10 @@ struct ChannelForm: View { } hasChanges = true } - .onChange(of: uplink) { _ in + .onChange(of: uplink) { hasChanges = true } - .onChange(of: downlink) { _ in + .onChange(of: downlink) { hasChanges = true } .onFirstAppear { diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 46211dac..bf036aca 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -45,7 +45,7 @@ struct BluetoothConfig: View { Label("bluetooth.mode.fixedpin", systemImage: "wallet.pass") TextField("bluetooth.mode.fixedpin", text: $fixedPin) .foregroundColor(.gray) - .onChange(of: fixedPin, perform: { _ in + .onChange(of: fixedPin) { // Don't let the first character be 0 because it will get stripped when saving a UInt32 if fixedPin.first == "0" { fixedPin = fixedPin.replacing("0", with: "") @@ -59,7 +59,7 @@ struct BluetoothConfig: View { } else if fixedPin.utf8.count < pinLength { shortPin = true } - }) + } .foregroundColor(.gray) } .keyboardType(.decimalPad) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 117c0683..9d20782c 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -95,7 +95,7 @@ struct DeviceConfig: View { Label("Time Zone", systemImage: "clock.badge.exclamationmark") TextField("Time Zone", text: $tzdef, axis: .vertical) .foregroundColor(.gray) - .onChange(of: tzdef) { _ in + .onChange(of: tzdef) { var totalBytes = tzdef.utf8.count // Only mess with the value if it is too big while totalBytes > 63 { diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 568be9da..dff48797 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -71,8 +71,7 @@ struct CannedMessagesConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: messages, perform: { _ in - + .onChange(of: messages) { var totalBytes = messages.utf8.count // Only mess with the value if it is too big while totalBytes > 198 { @@ -80,7 +79,7 @@ struct CannedMessagesConfig: View { totalBytes = messages.utf8.count } hasMessagesChanges = true - }) + } .foregroundColor(.gray) } .keyboardType(.default) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index c077c92e..67bbc248 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -123,14 +123,14 @@ struct MQTTConfig: View { Label("Root Topic", systemImage: "tree") TextField("Root Topic", text: $root) .foregroundColor(.gray) - .onChange(of: root, perform: { _ in + .onChange(of: root) { var totalBytes = root.utf8.count // Only mess with the value if it is too big while totalBytes > 30 { root = String(root.dropLast()) totalBytes = root.utf8.count } - }) + } .foregroundColor(.gray) } .keyboardType(.asciiCapable) @@ -162,7 +162,7 @@ struct MQTTConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: address, perform: { _ in + .onChange(of: address) { var totalBytes = address.utf8.count // Only mess with the value if it is too big while totalBytes > 62 { @@ -170,7 +170,7 @@ struct MQTTConfig: View { totalBytes = address.utf8.count } hasChanges = true - }) + } .keyboardType(.default) } .autocorrectionDisabled() @@ -181,7 +181,7 @@ struct MQTTConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: username, perform: { _ in + .onChange(of: username) { var totalBytes = username.utf8.count // Only mess with the value if it is too big while totalBytes > 62 { @@ -189,7 +189,7 @@ struct MQTTConfig: View { totalBytes = username.utf8.count } hasChanges = true - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -200,7 +200,7 @@ struct MQTTConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: password, perform: { _ in + .onChange(of: password) { var totalBytes = password.utf8.count // Only mess with the value if it is too big while totalBytes > 62 { @@ -208,7 +208,7 @@ struct MQTTConfig: View { totalBytes = password.utf8.count } hasChanges = true - }) + } .foregroundColor(.gray) } .keyboardType(.default) diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 1141bc7d..2fd97646 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -79,11 +79,11 @@ struct PaxCounterConfig: View { } } } - .onChange(of: enabled) { - if $0 != node?.paxCounterConfig?.enabled { hasChanges = true } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != node?.paxCounterConfig?.enabled { hasChanges = true } } - .onChange(of: paxcounterUpdateInterval) { - if $0 != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true } + .onChange(of: paxcounterUpdateInterval) { oldPaxcounterUpdateInterval, newPaxcounterUpdateInterval in + if oldPaxcounterUpdateInterval != newPaxcounterUpdateInterval && newPaxcounterUpdateInterval != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index aeee9601..a1217bcf 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -167,32 +167,26 @@ struct StoreForwardConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node?.storeForwardConfig != nil { - if newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true } } - .onChange(of: isRouter) { newIsRouter in - if node != nil && node?.storeForwardConfig != nil { - if newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true } - } + .onChange(of: isRouter) { oldIsRouter, newIsRouter in + if oldIsRouter != newIsRouter && newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true } } - .onChange(of: heartbeat) { newHeartbeat in - if node != nil && node?.storeForwardConfig != nil { - if newHeartbeat != node!.storeForwardConfig!.heartbeat { hasChanges = true } - } + .onChange(of: heartbeat) { oldHeartbeat, newHeartbeat in + if oldHeartbeat != newHeartbeat && newHeartbeat != node!.storeForwardConfig!.heartbeat { hasChanges = true } } - .onChange(of: records) { newRecords in + .onChange(of: records) { oldRecords, newRecords in if node != nil && node?.storeForwardConfig != nil { if newRecords != node!.storeForwardConfig!.records { hasChanges = true } } } - .onChange(of: historyReturnMax) { newHistoryReturnMax in + .onChange(of: historyReturnMax) { oldHistoryReturnMax, newHistoryReturnMax in if node != nil && node?.storeForwardConfig != nil { if newHistoryReturnMax != node!.storeForwardConfig!.historyReturnMax { hasChanges = true } } } - .onChange(of: historyReturnWindow) { newHistoryReturnWindow in + .onChange(of: historyReturnWindow) { oldHistoryReturnWindow, newHistoryReturnWindow in if node != nil && node?.storeForwardConfig != nil { if newHistoryReturnWindow != node!.storeForwardConfig!.historyReturnWindow { hasChanges = true } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 198ad1a0..b79740e2 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -45,7 +45,7 @@ struct NetworkConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: wifiSsid, perform: { _ in + .onChange(of: wifiSsid) { var totalBytes = wifiSsid.utf8.count // Only mess with the value if it is too big while totalBytes > 32 { @@ -53,7 +53,7 @@ struct NetworkConfig: View { totalBytes = wifiSsid.utf8.count } hasChanges = true - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -63,7 +63,7 @@ struct NetworkConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: wifiPsk, perform: { _ in + .onChange(of: wifiPsk) { var totalBytes = wifiPsk.utf8.count // Only mess with the value if it is too big while totalBytes > 63 { @@ -71,7 +71,7 @@ struct NetworkConfig: View { totalBytes = wifiPsk.utf8.count } hasChanges = true - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -154,10 +154,10 @@ struct NetworkConfig: View { .onChange(of: wifiEnabled) { if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true } } - .onChange(of: wifiSsid) { newSSID in + .onChange(of: wifiSsid) { _, newSSID in if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true } } - .onChange(of: wifiPsk) { newPsk in + .onChange(of: wifiPsk) { _, newPsk in if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true } } .onChange(of: wifiMode) { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 4497c1f7..541146cc 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -398,7 +398,7 @@ struct PositionConfig: View { } } } - .onChange(of: fixedPosition) { newFixed in + .onChange(of: fixedPosition) { _, newFixed in if supportedVersion { if let positionConfig = node?.positionConfig { /// Fixed Position is off to start diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 9ba0fe0a..83454820 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -232,13 +232,13 @@ private struct FloatField: View { TextField(title.localized, value: $typingNumber, format: .number) .foregroundColor(.gray) .multilineTextAlignment(.trailing) - .onChange(of: typingNumber, perform: { _ in + .onChange(of: typingNumber) { if isValid(typingNumber) { number = typingNumber } else { typingNumber = number } - }) + } .keyboardType(.decimalPad) .onAppear { typingNumber = number diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 95a07dfb..c7abe54b 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -118,7 +118,7 @@ struct SecurityConfig: View { .onChange(of: adminChannelEnabled) { if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } - .onChange(of: publicKey) { _ in + .onChange(of: publicKey) { let tempKey = Data(base64Encoded: publicKey) ?? Data() if tempKey.count == 32 { hasValidPublicKey = true @@ -127,7 +127,7 @@ struct SecurityConfig: View { } hasChanges = true } - .onChange(of: privateKey) { _ in + .onChange(of: privateKey) { let tempKey = Data(base64Encoded: privateKey) ?? Data() if tempKey.count == 32 { hasValidPrivateKey = true @@ -136,7 +136,7 @@ struct SecurityConfig: View { } hasChanges = true } - .onChange(of: adminKey) { key in + .onChange(of: adminKey) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() if key.isEmpty { hasValidAdminKey = true diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index ea2cdc41..82e7bb32 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -283,7 +283,7 @@ struct RouteRecorder: View { .onDisappear(perform: { UIApplication.shared.isIdleTimerDisabled = false }) - .onChange(of: locationsHandler.locationsArray.last) { newLoc in + .onChange(of: locationsHandler.locationsArray.last) { _, newLoc in if locationsHandler.isRecording { if let loc = newLoc { if recording != nil { diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 625c2a56..174221d2 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -57,7 +57,6 @@ struct Routes: View { } do { - guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } let routeName = selectedFile.lastPathComponent.dropLast(4) let lines = fileContent.components(separatedBy: "\n") @@ -175,14 +174,14 @@ struct Routes: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: name, perform: { _ in + .onChange(of: name) { var totalBytes = name.utf8.count // Only mess with the value if it is too big while totalBytes > 100 { name = String(name.dropLast()) totalBytes = name.utf8.count } - }) + } Toggle(isOn: $enabled) { Label("enabled", systemImage: "point.topleft.filled.down.to.point.bottomright.curvepath") @@ -236,16 +235,16 @@ struct Routes: View { .controlSize(.large) .disabled(!hasChanges) } - .onChange(of: name) { _ in + .onChange(of: name) { hasChanges = true } - .onChange(of: notes) { _ in + .onChange(of: notes) { hasChanges = true } - .onChange(of: enabled) { _ in + .onChange(of: enabled) { hasChanges = true } - .onChange(of: color) { _ in + .onChange(of: color) { hasChanges = true } Map { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 52a78b0a..ab8c97b3 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -161,7 +161,7 @@ struct Settings: View { Image(systemName: "light.max") } } - + NavigationLink(value: SettingsNavigationState.cannedMessages) { Label { Text("canned.messages") @@ -335,7 +335,6 @@ struct Settings: View { .foregroundColor(.red) } } - if !(node?.deviceConfig?.isManaged ?? false) { if bleManager.connectedPeripheral != nil { @@ -387,7 +386,7 @@ struct Settings: View { } } .pickerStyle(.navigationLink) - .onChange(of: selectedNode) { newValue in + .onChange(of: selectedNode) { _, newValue in if selectedNode > 0 { let node = nodes.first(where: { $0.num == newValue }) let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) @@ -483,7 +482,7 @@ struct Settings: View { Firmware(node: node) } } - .onChange(of: UserDefaults.preferredPeripheralNum ) { newConnectedNode in + .onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in preferredNodeNum = newConnectedNode if nodes.count > 1 { if selectedNode == 0 { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index e4b293a8..1e10c571 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -235,15 +235,15 @@ struct ShareChannels: View { .onAppear { generateChannelSet() } - .onChange(of: includeChannel0) { _ in generateChannelSet() } - .onChange(of: includeChannel1) { _ in generateChannelSet() } - .onChange(of: includeChannel2) { _ in generateChannelSet() } - .onChange(of: includeChannel3) { _ in generateChannelSet() } - .onChange(of: includeChannel4) { _ in generateChannelSet() } - .onChange(of: includeChannel5) { _ in generateChannelSet() } - .onChange(of: includeChannel6) { _ in generateChannelSet() } - .onChange(of: includeChannel7) { _ in generateChannelSet() } - .onChange(of: replaceChannels) { _ in generateChannelSet() } + .onChange(of: includeChannel0) { generateChannelSet() } + .onChange(of: includeChannel1) { generateChannelSet() } + .onChange(of: includeChannel2) { generateChannelSet() } + .onChange(of: includeChannel3) { generateChannelSet() } + .onChange(of: includeChannel4) { generateChannelSet() } + .onChange(of: includeChannel5) { generateChannelSet() } + .onChange(of: includeChannel6) { generateChannelSet() } + .onChange(of: includeChannel7) { generateChannelSet() } + .onChange(of: replaceChannels) { generateChannelSet() } } } func generateChannelSet() { diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index 72084954..daacb849 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -49,14 +49,14 @@ struct UserConfig: View { Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill") TextField("Long Name", text: $longName) - .onChange(of: longName, perform: { _ in + .onChange(of: longName) { var totalBytes = longName.utf8.count // Only mess with the value if it is too big while totalBytes > (isLicensed ? 6 : 36) { longName = String(longName.dropLast()) totalBytes = longName.utf8.count } - }) + } } .keyboardType(.default) .disableAutocorrection(true) @@ -74,14 +74,14 @@ struct UserConfig: View { Label("Short Name", systemImage: "circlebadge.fill") TextField("Short Name", text: $shortName) .foregroundColor(.gray) - .onChange(of: shortName, perform: { _ in + .onChange(of: shortName) { var totalBytes = shortName.utf8.count // Only mess with the value if it is too big if totalBytes > 4 { shortName = String(shortName.dropLast()) totalBytes = shortName.utf8.count } - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -197,17 +197,17 @@ struct UserConfig: View { self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.00 self.hasChanges = false } - .onChange(of: shortName) { newShort in + .onChange(of: shortName) { _, newShort in if node != nil && node!.user != nil { if newShort != node?.user!.shortName { hasChanges = true } } } - .onChange(of: longName) { newLong in + .onChange(of: longName) { _, newLong in if node != nil && node!.user != nil { if newLong != node?.user!.longName { hasChanges = true } } } - .onChange(of: isLicensed) { newIsLicensed in + .onChange(of: isLicensed) { _, newIsLicensed in if node != nil && node!.user != nil { if newIsLicensed != node?.user!.isLicensed { hasChanges = true @@ -219,10 +219,10 @@ struct UserConfig: View { } } } - .onChange(of: overrideFrequency) { _ in + .onChange(of: overrideFrequency) { hasChanges = true } - .onChange(of: txPower) { _ in + .onChange(of: txPower) { hasChanges = true } } diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 06099bd1..e16e3913 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -120,7 +120,6 @@ struct WidgetsLiveActivity: Widget { } } - struct WidgetsLiveActivity_Previews: PreviewProvider { static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) From 3dcec505ff222ea8bdd92a8927c8b36bb5e95ff2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Oct 2024 16:35:42 -0700 Subject: [PATCH 254/333] linter updates --- .../Nodes/Helpers/Map/WaypointForm.swift | 8 ++-- Meshtastic/Views/Nodes/MeshMap.swift | 13 +----- Meshtastic/Views/Nodes/NodeMap.swift | 22 +++++----- .../Config/Module/CannedMessagesConfig.swift | 2 +- .../Settings/Config/Module/SerialConfig.swift | 44 +++++++------------ .../Config/Module/StoreForwardConfig.swift | 14 ++---- .../Views/Settings/Config/PowerConfig.swift | 20 ++++----- 7 files changed, 46 insertions(+), 77 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 4261b0cc..9b163154 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -65,14 +65,14 @@ struct WaypointForm: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: name, perform: { _ in + .onChange(of: name) { var totalBytes = name.utf8.count // Only mess with the value if it is too big while totalBytes > 30 { name = String(name.dropLast()) totalBytes = name.utf8.count } - }) + } } HStack { Text("Description") @@ -83,14 +83,14 @@ struct WaypointForm: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: description, perform: { _ in + .onChange(of: description) { var totalBytes = description.utf8.count // Only mess with the value if it is too big while totalBytes > 100 { description = String(description.dropLast()) totalBytes = description.utf8.count } - }) + } } HStack { Text("Icon") diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 253a15fd..447f6fdd 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -139,7 +139,7 @@ struct MeshMap: View { guard case .map = router.navigationState.selectedTab else { return } // TODO: handle deep link for waypoints } - .onChange(of: selectedMapLayer) { newMapLayer in + .onChange(of: selectedMapLayer) { _, newMapLayer in switch selectedMapLayer { case .standard: UserDefaults.mapLayer = newMapLayer @@ -183,17 +183,6 @@ struct MeshMap: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) -// Button(action: { -// withAnimation { -// editingFilters = !editingFilters -// } -// }) { -// Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") -// .padding(.vertical, 5) -// } -// .tint(Color(UIColor.secondarySystemBackground)) -// .foregroundColor(.accentColor) -// .buttonStyle(.borderedProminent) } .controlSize(.regular) .padding(5) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 53b0aae3..9988f237 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -26,8 +26,6 @@ struct NodeMap: View { @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate -// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], -// predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) private var positions: FetchedResults @@ -97,7 +95,7 @@ struct NodeMap: View { } } .pickerStyle(SegmentedPickerStyle()) - .onChange(of: (selectedMapLayer)) { newMapLayer in + .onChange(of: selectedMapLayer) { _, newMapLayer in UserDefaults.mapLayer = newMapLayer } .padding(.top, 5) @@ -144,10 +142,10 @@ struct NodeMap: View { .font(.footnote) } } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedOverlayServer)) { newSelectedOverlayServer in - UserDefaults.mapOverlayServer = newSelectedOverlayServer - } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in + UserDefaults.mapOverlayServer = newSelectedOverlayServer + } Text(LocalizedStringKey(selectedOverlayServer.attribution)) .font(.footnote) .foregroundColor(.gray) @@ -160,7 +158,7 @@ struct NodeMap: View { Text("Enable Offline Maps") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onChange(of: enableOfflineMaps) { newEnableOfflineMaps in + .onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in UserDefaults.enableOfflineMaps = newEnableOfflineMaps if !enableOfflineMaps { if self.selectedMapLayer == .offline { @@ -176,10 +174,10 @@ struct NodeMap: View { Text(tsl.description) } } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedTileServer)) { newSelectedTileServer in - UserDefaults.mapTileServer = newSelectedTileServer - } + .pickerStyle(DefaultPickerStyle()) + .onChange(of: (selectedTileServer)) { _, newSelectedTileServer in + UserDefaults.mapTileServer = newSelectedTileServer + } Text("Attribution:") .fontWeight(.semibold) .font(.footnote) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index dff48797..699d22d4 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -254,7 +254,7 @@ struct CannedMessagesConfig: View { } } } - .onChange(of: configPreset) { newPreset in + .onChange(of: configPreset) { _, newPreset in if newPreset == 1 { diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 893ddca1..a7fd5bdb 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -157,41 +157,29 @@ struct SerialConfig: View { } } } - .onChange(of: enabled) { - if $0 != node?.serialConfig?.enabled { hasChanges = true } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != node?.serialConfig?.enabled ?? false { hasChanges = true } } - .onChange(of: echo) { - if $0 != node?.serialConfig?.echo { hasChanges = true } + .onChange(of: echo) { oldEcho, newEcho in + if oldEcho != newEcho && newEcho != node?.serialConfig?.echo ?? false { hasChanges = true } } - .onChange(of: rxd) { newRxd in - if node != nil && node!.serialConfig != nil { - if newRxd != node!.serialConfig!.rxd { hasChanges = true } - } + .onChange(of: rxd) { oldRxd, newRxd in + if oldRxd != newRxd && newRxd != node?.serialConfig?.rxd ?? -1 { hasChanges = true } } - .onChange(of: txd) { newTxd in - if node != nil && node!.serialConfig != nil { - if newTxd != node!.serialConfig!.txd { hasChanges = true } - } + .onChange(of: txd) { oldTxd, newTxd in + if oldTxd != newTxd && newTxd != node?.serialConfig?.txd ?? -1 { hasChanges = true } } - .onChange(of: baudRate) { newBaud in - if node != nil && node!.serialConfig != nil { - if newBaud != node!.serialConfig!.baudRate { hasChanges = true } - } + .onChange(of: baudRate) { oldBaud, newBaud in + if oldBaud != newBaud && newBaud != node?.serialConfig?.baudRate ?? -1 { hasChanges = true } } - .onChange(of: timeout) { newTimeout in - if node != nil && node!.serialConfig != nil { - if newTimeout != node!.serialConfig!.timeout { hasChanges = true } - } + .onChange(of: timeout) { oldTimeout, newTimeout in + if oldTimeout != newTimeout && newTimeout != node?.serialConfig?.timeout ?? -1 { hasChanges = true } } - .onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in - if node != nil && node!.serialConfig != nil { - if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true } - } + .onChange(of: overrideConsoleSerialPort) { oldOverrideConsoleSerialPort, newOverrideConsoleSerialPort in + if oldOverrideConsoleSerialPort != newOverrideConsoleSerialPort && newOverrideConsoleSerialPort != node?.serialConfig?.overrideConsoleSerialPort ?? false { hasChanges = true } } - .onChange(of: mode) { newMode in - if node != nil && node!.serialConfig != nil { - if newMode != node!.serialConfig!.mode { hasChanges = true } - } + .onChange(of: mode) { oldMode, newMode in + if oldMode != newMode && newMode != node?.serialConfig?.mode ?? -1 { hasChanges = true } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index a1217bcf..d90ea3fb 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -174,22 +174,16 @@ struct StoreForwardConfig: View { if oldIsRouter != newIsRouter && newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true } } .onChange(of: heartbeat) { oldHeartbeat, newHeartbeat in - if oldHeartbeat != newHeartbeat && newHeartbeat != node!.storeForwardConfig!.heartbeat { hasChanges = true } + if oldHeartbeat != newHeartbeat && newHeartbeat != node?.storeForwardConfig?.heartbeat ?? true { hasChanges = true } } .onChange(of: records) { oldRecords, newRecords in - if node != nil && node?.storeForwardConfig != nil { - if newRecords != node!.storeForwardConfig!.records { hasChanges = true } - } + if oldRecords != newRecords && newRecords != node!.storeForwardConfig?.records ?? -1 { hasChanges = true } } .onChange(of: historyReturnMax) { oldHistoryReturnMax, newHistoryReturnMax in - if node != nil && node?.storeForwardConfig != nil { - if newHistoryReturnMax != node!.storeForwardConfig!.historyReturnMax { hasChanges = true } - } + if oldHistoryReturnMax != newHistoryReturnMax && newHistoryReturnMax != node!.storeForwardConfig?.historyReturnMax ?? -1 { hasChanges = true } } .onChange(of: historyReturnWindow) { oldHistoryReturnWindow, newHistoryReturnWindow in - if node != nil && node?.storeForwardConfig != nil { - if newHistoryReturnWindow != node!.storeForwardConfig!.historyReturnWindow { hasChanges = true } - } + if oldHistoryReturnWindow != newHistoryReturnWindow && newHistoryReturnWindow != node!.storeForwardConfig?.historyReturnWindow ?? -1 { hasChanges = true } } } func setStoreAndForwardValues() { diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 83454820..b50368ab 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -148,25 +148,25 @@ struct PowerConfig: View { } } } - .onChange(of: isPowerSaving) { - if $0 != node?.powerConfig?.isPowerSaving { hasChanges = true } + .onChange(of: isPowerSaving) { oldIsPowerSaving, newIsPowerSaving in + if oldIsPowerSaving != newIsPowerSaving && newIsPowerSaving != node?.powerConfig?.isPowerSaving { hasChanges = true } } - .onChange(of: shutdownOnPowerLoss) { newShutdownOnPowerLoss in + .onChange(of: shutdownOnPowerLoss) { _, newShutdownOnPowerLoss in if newShutdownOnPowerLoss { hasChanges = true } } - .onChange(of: shutdownAfterSecs) { - if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } + .onChange(of: shutdownAfterSecs) { oldShutdownAfterSecs, newShutdownAfterSecs in + if oldShutdownAfterSecs != newShutdownAfterSecs && newShutdownAfterSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } - .onChange(of: adcOverride) { _ in + .onChange(of: adcOverride) { hasChanges = true } - .onChange(of: adcMultiplier) { newAdcMultiplier in - if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true } + .onChange(of: adcMultiplier) { _, newAdcMultiplier in + if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true } } - .onChange(of: waitBluetoothSecs) { - if $0 != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true } + .onChange(of: waitBluetoothSecs) { oldWaitBluetoothSecs, newWaitBluetoothSecs in + if oldWaitBluetoothSecs != newWaitBluetoothSecs && newWaitBluetoothSecs != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true } } .onChange(of: lsSecs) { if $0 != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true } From d7e887dd636acb0230c4c14037ad795e9274d7d1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 6 Oct 2024 08:50:12 -0700 Subject: [PATCH 255/333] Update onchange events --- .../Views/MapKitMap/WaypointFormMapKit.swift | 6 +- .../Nodes/Helpers/Map/WaypointForm.swift | 2 +- .../Settings/Config/BluetoothConfig.swift | 12 +-- .../Views/Settings/Config/DeviceConfig.swift | 22 +++-- .../Views/Settings/Config/DisplayConfig.swift | 36 ++++---- .../Views/Settings/Config/LoRaConfig.swift | 38 ++++---- .../Config/Module/AmbientLightingConfig.swift | 18 ++-- .../Config/Module/CannedMessagesConfig.swift | 60 +++++-------- .../Config/Module/DetectionSensorConfig.swift | 54 ++++------- .../Module/ExternalNotificationConfig.swift | 90 +++++++------------ .../Settings/Config/Module/MQTTConfig.swift | 32 +++---- .../Config/Module/RangeTestConfig.swift | 12 +-- .../Settings/Config/Module/RtttlConfig.swift | 6 +- .../Views/Settings/Config/NetworkConfig.swift | 12 +-- .../Settings/Config/PositionConfig.swift | 20 ++--- .../Views/Settings/Config/PowerConfig.swift | 8 +- .../Settings/Config/SecurityConfig.swift | 16 ++-- 17 files changed, 183 insertions(+), 261 deletions(-) diff --git a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index f2451a28..ed48e1a4 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -72,14 +72,14 @@ struct WaypointFormMapKit: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: description, perform: { _ in + .onChange(of: description) { var totalBytes = description.utf8.count // Only mess with the value if it is too big while totalBytes > 100 { description = String(description.dropLast()) totalBytes = description.utf8.count } - }) + } } HStack { Text("Icon") @@ -87,7 +87,7 @@ struct WaypointFormMapKit: View { EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") .font(.title) .focused($iconIsFocused) - .onChange(of: icon) { value in + .onChange(of: icon) { _, value in // If you have anything other than emojis in your string make it empty if !value.onlyEmojis() { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 9b163154..71d62590 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -98,7 +98,7 @@ struct WaypointForm: View { EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") .font(.title) .focused($iconIsFocused) - .onChange(of: icon) { value in + .onChange(of: icon) { _, value in // If you have anything other than emojis in your string make it empty if !value.onlyEmojis() { diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index bf036aca..37f989a5 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -121,14 +121,14 @@ struct BluetoothConfig: View { } } } - .onChange(of: enabled) { - if $0 != node?.bluetoothConfig?.enabled { hasChanges = true } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != node?.bluetoothConfig?.enabled { hasChanges = true } } - .onChange(of: mode) { - if $0 != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true } + .onChange(of: mode) { oldNode, newNode in + if oldNode != newNode && newNode != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true } } - .onChange(of: fixedPin) { newFixedPin in - if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true } + .onChange(of: fixedPin) { oldFixedPin, newFixedPin in + if oldFixedPin != newFixedPin && newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true } } } func setBluetoothValues() { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 9d20782c..f5fca283 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -261,22 +261,20 @@ struct DeviceConfig: View { .onChange(of: rebroadcastMode) { oldRebroadcastMode, newRebroadcastMode in if oldRebroadcastMode != newRebroadcastMode && newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true } } - .onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in - if newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true } + .onChange(of: nodeInfoBroadcastSecs) { oldNodeInfoBroadcastSecs, newNodeInfoBroadcastSecs in + if oldNodeInfoBroadcastSecs != newNodeInfoBroadcastSecs && newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true } } - .onChange(of: doubleTapAsButtonPress) { - if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } + .onChange(of: doubleTapAsButtonPress) { oldDoubleTapAsButtonPress, newDoubleTapAsButtonPress in + if oldDoubleTapAsButtonPress != newDoubleTapAsButtonPress && newDoubleTapAsButtonPress != node?.deviceConfig?.doubleTapAsButtonPress ?? false { hasChanges = true } } - .onChange(of: tripleClickAsAdHocPing) { - if $0 != node?.deviceConfig?.tripleClickAsAdHocPing { hasChanges = true } + .onChange(of: tripleClickAsAdHocPing) { oldTripleClickAsAdHocPing, newTripleClickAsAdHocPing in + if oldTripleClickAsAdHocPing != newTripleClickAsAdHocPing && newTripleClickAsAdHocPing != node?.deviceConfig?.tripleClickAsAdHocPing ?? false { hasChanges = true } } - .onChange(of: tzdef) { newTzdef in - if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } + .onChange(of: tzdef) { oldTzdef, newTzdef in + if oldTzdef != newTzdef && newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } } - .onChange(of: ledHeartbeatEnabled) { newLedHeartbeatEnabled in - if node != nil && node?.deviceConfig != nil { - if newLedHeartbeatEnabled != node!.deviceConfig!.ledHeartbeatEnabled { hasChanges = true } - } + .onChange(of: ledHeartbeatEnabled) { oldLedHeartbeatEnabled, newLedHeartbeatEnabled in + if oldLedHeartbeatEnabled != newLedHeartbeatEnabled && newLedHeartbeatEnabled != node?.deviceConfig?.ledHeartbeatEnabled ?? false { hasChanges = true } } } func setDeviceValues() { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 1e9781a6..07975419 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -184,32 +184,32 @@ struct DisplayConfig: View { } } } - .onChange(of: screenOnSeconds) { newScreenSecs in - if newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true } + .onChange(of: screenOnSeconds) { oldScreenSecs, newScreenSecs in + if oldScreenSecs != newScreenSecs && newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true } } - .onChange(of: screenCarouselInterval) { newCarouselSecs in - if newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true } + .onChange(of: screenCarouselInterval) { oldCarouselSecs, newCarouselSecs in + if oldCarouselSecs != newCarouselSecs && newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true } } - .onChange(of: compassNorthTop) { - if $0 != node?.displayConfig?.compassNorthTop { hasChanges = true } + .onChange(of: compassNorthTop) { oldCompassNorthTop, newCompassNorthTop in + if oldCompassNorthTop != newCompassNorthTop && newCompassNorthTop != node?.displayConfig?.compassNorthTop { hasChanges = true } } - .onChange(of: wakeOnTapOrMotion) { - if $0 != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true } + .onChange(of: wakeOnTapOrMotion) { oldWakeOnTapOrMotion, newWakeOnTapOrMotion in + if oldWakeOnTapOrMotion != newWakeOnTapOrMotion && newWakeOnTapOrMotion != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true } } - .onChange(of: gpsFormat) { newGpsFormat in - if newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true } + .onChange(of: gpsFormat) { oldGpsFormat, newGpsFormat in + if oldGpsFormat != newGpsFormat && newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true } } - .onChange(of: flipScreen) { - if $0 != node?.displayConfig?.flipScreen { hasChanges = true } + .onChange(of: flipScreen) { oldFlipScreen, newFlipScreen in + if oldFlipScreen != newFlipScreen && newFlipScreen != node?.displayConfig?.flipScreen { hasChanges = true } } - .onChange(of: oledType) { newOledType in - if newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true } + .onChange(of: oledType) { oldOledType, newOledType in + if oldOledType != newOledType && newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true } } - .onChange(of: displayMode) { newDisplayMode in - if newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true } + .onChange(of: displayMode) { oldDisplayMode, newDisplayMode in + if oldDisplayMode != newDisplayMode && newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true } } - .onChange(of: units) { newUnits in - if newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true } + .onChange(of: units) { oldUnits, newUnits in + if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true } } } func setDisplayValues() { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 13623ee3..4c12c598 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -259,47 +259,47 @@ struct LoRaConfig: View { } } } - .onChange(of: region) { newRegion in + .onChange(of: region) { _, newRegion in if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true } } - .onChange(of: usePreset) { - if $0 != node?.loRaConfig?.usePreset { hasChanges = true } + .onChange(of: usePreset) { _, newPreset in + if newPreset != node?.loRaConfig?.usePreset { hasChanges = true } } - .onChange(of: modemPreset) { newModemPreset in + .onChange(of: modemPreset) { _, newModemPreset in if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true } } - .onChange(of: hopLimit) { newHopLimit in + .onChange(of: hopLimit) { _, newHopLimit in if newHopLimit != node?.loRaConfig?.hopLimit ?? -1 { hasChanges = true } } - .onChange(of: channelNum) { newChannelNum in + .onChange(of: channelNum) { _, newChannelNum in if newChannelNum != node?.loRaConfig?.channelNum ?? -1 { hasChanges = true } } - .onChange(of: bandwidth) { newBandwidth in + .onChange(of: bandwidth) { _, newBandwidth in if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true } } - .onChange(of: codingRate) { newCodingRate in + .onChange(of: codingRate) { _, newCodingRate in if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true } } - .onChange(of: spreadFactor) { newSpreadFactor in + .onChange(of: spreadFactor) { _, newSpreadFactor in if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true } } - .onChange(of: rxBoostedGain) { - if $0 != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true } + .onChange(of: rxBoostedGain) { _, newRxBoostedGain in + if newRxBoostedGain != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true } } - .onChange(of: overrideFrequency) { newOverrideFrequency in + .onChange(of: overrideFrequency) { _, newOverrideFrequency in if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true } } - .onChange(of: txPower) { newTxPower in + .onChange(of: txPower) { _, newTxPower in if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true } } - .onChange(of: txEnabled) { - if $0 != node?.loRaConfig?.txEnabled { hasChanges = true } + .onChange(of: txEnabled) { _, newTxEnabled in + if newTxEnabled != node?.loRaConfig?.txEnabled { hasChanges = true } } - .onChange(of: ignoreMqtt) { - if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true } + .onChange(of: ignoreMqtt) { _, newIgnoreMqtt in + if newIgnoreMqtt != node?.loRaConfig?.ignoreMqtt { hasChanges = true } } - .onChange(of: okToMqtt) { - if $0 != node?.loRaConfig?.okToMqtt { hasChanges = true } + .onChange(of: okToMqtt) { _, newOkToMqtt in + if newOkToMqtt != node?.loRaConfig?.okToMqtt { hasChanges = true } } } func setLoRaValues() { diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 5e82da1b..fc82a4ca 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -106,20 +106,14 @@ struct AmbientLightingConfig: View { } } } - .onChange(of: ledState) { - if let val = node?.ambientLightingConfig?.ledState { - hasChanges = $0 != val - } + .onChange(of: ledState) { _, newLedState in + if newLedState != node?.ambientLightingConfig?.ledState { hasChanges = true } } - .onChange(of: current) { - if let val = node?.ambientLightingConfig?.current { - hasChanges = $0 != val - } + .onChange(of: current) { _, newCurrent in + if newCurrent != node?.ambientLightingConfig?.current ?? 10 { hasChanges = true } } - .onChange(of: color) { c in - if color != c { - hasChanges = true - } + .onChange(of: color) { oldColor, newColor in + if oldColor != newColor { hasChanges = true } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 699d22d4..4f580b2a 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -283,55 +283,35 @@ struct CannedMessagesConfig: View { hasChanges = true } - .onChange(of: enabled) { - if let val = node?.cannedMessageConfig?.enabled { - hasChanges = $0 != val - } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.cannedMessageConfig?.enabled { hasChanges = true } } - .onChange(of: sendBell) { - if let val = node?.cannedMessageConfig?.sendBell { - hasChanges = $0 != val - } + .onChange(of: sendBell) { _, newSendBell in + if newSendBell != node?.cannedMessageConfig?.sendBell { hasChanges = true } } - .onChange(of: rotary1Enabled) { - if let val = node?.cannedMessageConfig?.rotary1Enabled { - hasChanges = $0 != val - } + .onChange(of: rotary1Enabled) { _, newRotary1Enabled in + if newRotary1Enabled != node?.cannedMessageConfig?.rotary1Enabled { hasChanges = true } } - .onChange(of: updown1Enabled) { - if let val = node?.cannedMessageConfig?.updown1Enabled { - hasChanges = $0 != val - } + .onChange(of: updown1Enabled) { _, newUpdown1Enabled in + if newUpdown1Enabled != node?.cannedMessageConfig?.updown1Enabled { hasChanges = true } } - .onChange(of: inputbrokerPinA) { newPinA in - if node != nil && node!.cannedMessageConfig != nil { - if newPinA != node!.cannedMessageConfig!.inputbrokerPinA { hasChanges = true } - } + .onChange(of: inputbrokerPinA) { _, newPinA in + if newPinA != node?.cannedMessageConfig?.inputbrokerPinA ?? -1 { hasChanges = true } } - .onChange(of: inputbrokerPinB) { newPinB in - if node != nil && node!.cannedMessageConfig != nil { - if newPinB != node!.cannedMessageConfig!.inputbrokerPinB { hasChanges = true } - } + .onChange(of: inputbrokerPinB) { _, newPinB in + if newPinB != node?.cannedMessageConfig?.inputbrokerPinB ?? -1 { hasChanges = true } } - .onChange(of: inputbrokerPinPress) { newPinPress in - if node != nil && node!.cannedMessageConfig != nil { - if newPinPress != node!.cannedMessageConfig!.inputbrokerPinPress { hasChanges = true } - } + .onChange(of: inputbrokerPinPress) { _, newPinPress in + if newPinPress != node?.cannedMessageConfig?.inputbrokerPinPress ?? -1 { hasChanges = true } } - .onChange(of: inputbrokerEventCw) { newKeyA in - if node != nil && node!.cannedMessageConfig != nil { - if newKeyA != node!.cannedMessageConfig!.inputbrokerEventCw { hasChanges = true } - } + .onChange(of: inputbrokerEventCw) { _, newKeyA in + if newKeyA != node?.cannedMessageConfig?.inputbrokerEventCw ?? -1 { hasChanges = true } } - .onChange(of: inputbrokerEventCcw) { newKeyB in - if node != nil && node!.cannedMessageConfig != nil { - if newKeyB != node!.cannedMessageConfig!.inputbrokerEventCcw { hasChanges = true } - } + .onChange(of: inputbrokerEventCcw) { _, newKeyB in + if newKeyB != node?.cannedMessageConfig?.inputbrokerEventCcw ?? -1 { hasChanges = true } } - .onChange(of: inputbrokerEventPress) { newKeyPress in - if node != nil && node!.cannedMessageConfig != nil { - if newKeyPress != node!.cannedMessageConfig!.inputbrokerEventPress { hasChanges = true } - } + .onChange(of: inputbrokerEventPress) { _, newKeyPress in + if newKeyPress != node?.cannedMessageConfig?.inputbrokerEventPress ?? -1 { hasChanges = true } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 6305dd98..eb50a80f 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -91,14 +91,14 @@ struct DetectionSensorConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: name, perform: { _ in + .onChange(of: name) { var totalBytes = name.utf8.count // Only mess with the value if it is too big while totalBytes > 20 { name = String(name.dropLast()) totalBytes = name.utf8.count } - }) + } } .listRowSeparator(.hidden) Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"") @@ -210,47 +210,31 @@ struct DetectionSensorConfig: View { } } } - .onChange(of: enabled) { - if let val = node?.detectionSensorConfig?.enabled { - hasChanges = $0 != val - } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.detectionSensorConfig?.enabled { hasChanges = true } } - .onChange(of: sendBell) { - if let val = node?.detectionSensorConfig?.sendBell { - hasChanges = $0 != val - } + .onChange(of: sendBell) { _, newSendBell in + if newSendBell != node?.detectionSensorConfig?.sendBell { hasChanges = true } } - .onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in - if node != nil && node?.detectionSensorConfig != nil { - if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true } - } + .onChange(of: detectionTriggeredHigh) { _, newDetectionTriggeredHigh in + if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true } } - .onChange(of: usePullup) { - if let val = node?.detectionSensorConfig?.usePullup { - hasChanges = $0 != val - } + .onChange(of: usePullup) { _, newUsePullup in + if newUsePullup != node?.detectionSensorConfig?.usePullup { hasChanges = true } } - .onChange(of: name) { newName in - if node != nil && node?.detectionSensorConfig != nil { - if newName != node!.detectionSensorConfig!.name { hasChanges = true } - } + .onChange(of: name) { _, newName in + if newName != node?.detectionSensorConfig?.name ?? "" { hasChanges = true } } - .onChange(of: monitorPin) { newMonitorPin in - if node != nil && node?.detectionSensorConfig != nil { - if newMonitorPin != node!.detectionSensorConfig!.monitorPin { hasChanges = true } - } + .onChange(of: monitorPin) { _, newMonitorPin in + if newMonitorPin != node?.detectionSensorConfig?.monitorPin ?? 0 { hasChanges = true } } - .onChange(of: minimumBroadcastSecs) { newMinimumBroadcastSecs in - if node != nil && node?.detectionSensorConfig != nil { - if newMinimumBroadcastSecs != node!.detectionSensorConfig!.minimumBroadcastSecs { hasChanges = true } - } + .onChange(of: minimumBroadcastSecs) { _, newMinimumBroadcastSecs in + if newMinimumBroadcastSecs != node?.detectionSensorConfig?.minimumBroadcastSecs ?? 0 { hasChanges = true } } - .onChange(of: stateBroadcastSecs) { newStateBroadcastSecs in - if node != nil && node?.detectionSensorConfig != nil { - if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true } - } + .onChange(of: stateBroadcastSecs) { _, newStateBroadcastSecs in + if newStateBroadcastSecs != node?.detectionSensorConfig?.stateBroadcastSecs ?? 0 { hasChanges = true } } - .onChange(of: detectionNotificationsEnabled) { newDetectionNotificationsEnabled in + .onChange(of: detectionNotificationsEnabled) { _, newDetectionNotificationsEnabled in UserDefaults.enableDetectionNotifications = newDetectionNotificationsEnabled } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 57c3a672..a0f14c7f 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -220,80 +220,50 @@ struct ExternalNotificationConfig: View { } } } - .onChange(of: enabled) { - if let val = node?.externalNotificationConfig?.enabled { - hasChanges = $0 != val - } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.externalNotificationConfig?.enabled { hasChanges = true } } - .onChange(of: alertBell) { - if let val = node?.externalNotificationConfig?.alertBell { - hasChanges = $0 != val - } + .onChange(of: alertBell) { _, newAlertBell in + if newAlertBell != node?.externalNotificationConfig?.alertBell { hasChanges = true } } - .onChange(of: alertBellBuzzer) { - if let val = node?.externalNotificationConfig?.alertBellBuzzer { - hasChanges = $0 != val - } + .onChange(of: alertBellBuzzer) { _, newAlertBellBuzzer in + if newAlertBellBuzzer != node?.externalNotificationConfig?.alertBellBuzzer { hasChanges = true } } - .onChange(of: alertBellVibra) { - if let val = node?.externalNotificationConfig?.alertBellVibra { - hasChanges = $0 != val - } + .onChange(of: alertBellVibra) { _, newAlertBellVibra in + if newAlertBellVibra != node?.externalNotificationConfig?.alertBellVibra { hasChanges = true } } - .onChange(of: alertMessage) { - if let val = node?.externalNotificationConfig?.alertMessage { - hasChanges = $0 != val - } + .onChange(of: alertMessage) { _, newAlertMessage in + if newAlertMessage != node?.externalNotificationConfig?.alertMessage { hasChanges = true } } - .onChange(of: alertMessageBuzzer) { - if let val = node?.externalNotificationConfig?.alertMessageBuzzer { - hasChanges = $0 != val - } + .onChange(of: alertMessageBuzzer) { _, newAlertMessageBuzzer in + if newAlertMessageBuzzer != node?.externalNotificationConfig?.alertMessageBuzzer { hasChanges = true } } - .onChange(of: alertMessageVibra) { - if let val = node?.externalNotificationConfig?.alertMessageVibra { - hasChanges = $0 != val - } + .onChange(of: alertMessageVibra) { _, newAlertMessageVibra in + if newAlertMessageVibra != node?.externalNotificationConfig?.alertMessageVibra { hasChanges = true } } - .onChange(of: active) { - if let val = node?.externalNotificationConfig?.active { - hasChanges = $0 != val - } + .onChange(of: active) { _, newActive in + if newActive != node?.externalNotificationConfig?.active { hasChanges = true } } - .onChange(of: output) { newOutput in - if node != nil && node!.externalNotificationConfig != nil { - if newOutput != node!.externalNotificationConfig!.output { hasChanges = true } - } + .onChange(of: output) { _, newOutput in + if newOutput != node?.externalNotificationConfig?.output ?? -1 { hasChanges = true } } - .onChange(of: output) { newOutputBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newOutputBuzzer != node!.externalNotificationConfig!.outputBuzzer { hasChanges = true } - } + .onChange(of: output) { _, newOutputBuzzer in + if newOutputBuzzer != node?.externalNotificationConfig?.outputBuzzer ?? -1 { hasChanges = true } } - .onChange(of: output) { newOutputVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newOutputVibra != node!.externalNotificationConfig!.outputVibra { hasChanges = true } - } + .onChange(of: output) { _, newOutputVibra in + if newOutputVibra != node?.externalNotificationConfig?.outputVibra ?? -1 { hasChanges = true } } - .onChange(of: outputMilliseconds) { newOutputMs in - if node != nil && node!.externalNotificationConfig != nil { - if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } - } + .onChange(of: outputMilliseconds) { _, newOutputMs in + if newOutputMs != node?.externalNotificationConfig?.outputMilliseconds ?? -1 { hasChanges = true } } - .onChange(of: usePWM) { - if let val = node?.externalNotificationConfig?.usePWM { - hasChanges = $0 != val - } + .onChange(of: usePWM) { _, newPWM in + if newPWM != node?.externalNotificationConfig?.usePWM { hasChanges = true } } - .onChange(of: nagTimeout) { newNagTimeout in - if node != nil && node!.externalNotificationConfig != nil { - if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } - } + .onChange(of: nagTimeout) { _, newNagTimeout in + if newNagTimeout != node?.externalNotificationConfig?.nagTimeout ?? -1 { hasChanges = true } } - .onChange(of: useI2SAsBuzzer) { - if let val = node?.externalNotificationConfig?.useI2SAsBuzzer { - hasChanges = $0 != val - } + .onChange(of: useI2SAsBuzzer) { _, newUseI2SAsBuzzer in + if newUseI2SAsBuzzer != node?.externalNotificationConfig?.useI2SAsBuzzer { hasChanges = true } } } func setExternalNotificationValues() { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 67bbc248..50eb5806 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -262,24 +262,20 @@ struct MQTTConfig: View { ) } ) - .onChange(of: enabled) { - if $0 != node?.mqttConfig?.enabled { hasChanges = true } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.mqttConfig?.enabled { hasChanges = true } } - .onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in + .onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in if newProxyToClientEnabled { jsonEnabled = false } if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true } } - .onChange(of: address) { newAddress in - if node != nil && node?.mqttConfig != nil { - if newAddress != node!.mqttConfig!.address { hasChanges = true } - } + .onChange(of: address) { _, newAddress in + if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true } } .onChange(of: username) { newUsername in - if node != nil && node?.mqttConfig != nil { - if newUsername != node!.mqttConfig!.username { hasChanges = true } - } + if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true } } .onChange(of: password) { newPassword in if node != nil && node?.mqttConfig != nil { @@ -291,26 +287,26 @@ struct MQTTConfig: View { if newRoot != node!.mqttConfig!.root { hasChanges = true } } } - .onChange(of: selectedTopic) { newSelectedTopic in + .onChange(of: selectedTopic) { _, newSelectedTopic in root = newSelectedTopic } - .onChange(of: encryptionEnabled) { - if $0 != node?.mqttConfig?.encryptionEnabled { hasChanges = true } + .onChange(of: encryptionEnabled) { _, newEncryptionEnabled in + if newEncryptionEnabled != node?.mqttConfig?.encryptionEnabled { hasChanges = true } } - .onChange(of: jsonEnabled) { newJsonEnabled in + .onChange(of: jsonEnabled) { _, newJsonEnabled in if newJsonEnabled { proxyToClientEnabled = false } if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true } } - .onChange(of: tlsEnabled) { newTlsEnabled in + .onChange(of: tlsEnabled) { _, newTlsEnabled in if address.lowercased() == "mqtt.meshtastic.org" { tlsEnabled = false } else { if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true } } } - .onChange(of: mqttConnected) { newMqttConnected in + .onChange(of: mqttConnected) { _, newMqttConnected in if newMqttConnected == false { if bleManager.mqttProxyConnected { bleManager.mqttManager.disconnect() @@ -321,8 +317,8 @@ struct MQTTConfig: View { } } } - .onChange(of: mapReportingEnabled) { - if $0 != node?.mqttConfig?.mapReportingEnabled { hasChanges = true } + .onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in + if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true } } .onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in if node != nil && node?.mqttConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 872294d8..ae4797fc 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -102,14 +102,14 @@ struct RangeTestConfig: View { } } } - .onChange(of: enabled) { - if $0 != node?.rangeTestConfig?.enabled { hasChanges = true } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.rangeTestConfig?.enabled { hasChanges = true } } - .onChange(of: save) { - if $0 != node?.rangeTestConfig?.save { hasChanges = true } + .onChange(of: save) { _, newSave in + if newSave != node?.rangeTestConfig?.save { hasChanges = true } } - .onChange(of: sender) { - if $0 != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true } + .onChange(of: sender) { _, newSender in + if newSender != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 772b6b98..2e0931a7 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -31,14 +31,14 @@ struct RtttlConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: ringtone, perform: { _ in + .onChange(of: ringtone) { var totalBytes = ringtone.utf8.count // Only mess with the value if it is too big while totalBytes > 228 { ringtone = String(ringtone.dropLast()) totalBytes = ringtone.utf8.count } - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -93,7 +93,7 @@ struct RtttlConfig: View { } } } - .onChange(of: ringtone) { newRingtone in + .onChange(of: ringtone) { _, newRingtone in if node != nil && node!.rtttlConfig != nil { if newRingtone != node!.rtttlConfig!.ringtone { hasChanges = true } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index b79740e2..8cebde85 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -151,8 +151,8 @@ struct NetworkConfig: View { } } } - .onChange(of: wifiEnabled) { - if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true } + .onChange(of: wifiEnabled) { _, newEnabled in + if newEnabled != node?.networkConfig?.wifiEnabled { hasChanges = true } } .onChange(of: wifiSsid) { _, newSSID in if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true } @@ -160,11 +160,11 @@ struct NetworkConfig: View { .onChange(of: wifiPsk) { _, newPsk in if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true } } - .onChange(of: wifiMode) { - if $0 != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true } + .onChange(of: wifiMode) { _, newMode in + if newMode != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true } } - .onChange(of: ethEnabled) { - if $0 != node?.networkConfig?.ethEnabled { hasChanges = true } + .onChange(of: ethEnabled) { _, newEthEnabled in + if newEthEnabled != node?.networkConfig?.ethEnabled { hasChanges = true } } } func setNetworkValues() { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 541146cc..fe61d1fa 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -411,37 +411,37 @@ struct PositionConfig: View { } } } - .onChange(of: gpsMode) { newGpsMode in + .onChange(of: gpsMode) { _, newGpsMode in if newGpsMode != node?.positionConfig?.gpsMode ?? 0 { hasChanges = true } } - .onChange(of: rxGpio) { newRxGpio in + .onChange(of: rxGpio) { _, newRxGpio in if newRxGpio != node?.positionConfig?.rxGpio ?? 0 { hasChanges = true } } - .onChange(of: txGpio) { newTxGpio in + .onChange(of: txGpio) { _, newTxGpio in if newTxGpio != node?.positionConfig?.txGpio ?? 0 { hasChanges = true } } - .onChange(of: gpsEnGpio) { newGpsEnGpio in + .onChange(of: gpsEnGpio) { _, newGpsEnGpio in if newGpsEnGpio != node?.positionConfig?.gpsEnGpio ?? 0 { hasChanges = true } } - .onChange(of: smartPositionEnabled) { newSmartPositionEnabled in + .onChange(of: smartPositionEnabled) { _, newSmartPositionEnabled in if newSmartPositionEnabled != node?.positionConfig?.smartPositionEnabled { hasChanges = true } } - .onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in + .onChange(of: positionBroadcastSeconds) { _, newPositionBroadcastSeconds in if newPositionBroadcastSeconds != node?.positionConfig?.positionBroadcastSeconds ?? 0 { hasChanges = true } } - .onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in + .onChange(of: broadcastSmartMinimumIntervalSecs) { _, newBroadcastSmartMinimumIntervalSecs in if newBroadcastSmartMinimumIntervalSecs != node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 0 { hasChanges = true } } - .onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in + .onChange(of: broadcastSmartMinimumDistance) { _, newBroadcastSmartMinimumDistance in if newBroadcastSmartMinimumDistance != node?.positionConfig?.broadcastSmartMinimumDistance ?? 0 { hasChanges = true } } - .onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in + .onChange(of: gpsUpdateInterval) { _, newGpsUpdateInterval in if newGpsUpdateInterval != node?.positionConfig?.gpsUpdateInterval ?? 0 { hasChanges = true } } } func handlePositionFlagtChanges() { - guard let positionConfig = node?.positionConfig else { return } + guard (node?.positionConfig) != nil else { return } let pf = PositionFlags(rawValue: self.positionFlags) hasChanges = pf.contains(.Altitude) || diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index b50368ab..822fd1e0 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -168,11 +168,11 @@ struct PowerConfig: View { .onChange(of: waitBluetoothSecs) { oldWaitBluetoothSecs, newWaitBluetoothSecs in if oldWaitBluetoothSecs != newWaitBluetoothSecs && newWaitBluetoothSecs != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true } } - .onChange(of: lsSecs) { - if $0 != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true } + .onChange(of: lsSecs) { _, newLsSecs in + if newLsSecs != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true } } - .onChange(of: minWakeSecs) { - if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } + .onChange(of: minWakeSecs) { _, newMinWakeSecs in + if newMinWakeSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index c7abe54b..75b69463 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -106,17 +106,17 @@ struct SecurityConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) - .onChange(of: isManaged) { - if $0 != node?.securityConfig?.isManaged { hasChanges = true } + .onChange(of: isManaged) { _, newIsManaged in + if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true } } - .onChange(of: serialEnabled) { - if $0 != node?.securityConfig?.serialEnabled { hasChanges = true } + .onChange(of: serialEnabled) { _, newSerialEnabled in + if newSerialEnabled != node?.securityConfig?.serialEnabled { hasChanges = true } } - .onChange(of: debugLogApiEnabled) { - if $0 != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } + .onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in + if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } } - .onChange(of: adminChannelEnabled) { - if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } + .onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in + if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true } } .onChange(of: publicKey) { let tempKey = Data(base64Encoded: publicKey) ?? Data() From e89dc2194241ccd34024122899ccba582bd83fa5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 6 Oct 2024 09:07:55 -0700 Subject: [PATCH 256/333] Finish upgrading onchange events --- .../Settings/Config/Module/MQTTConfig.swift | 14 ++---- .../Config/Module/TelemetryConfig.swift | 48 +++++++------------ 2 files changed, 20 insertions(+), 42 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 50eb5806..224fd43c 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -278,14 +278,10 @@ struct MQTTConfig: View { if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true } } .onChange(of: password) { newPassword in - if node != nil && node?.mqttConfig != nil { - if newPassword != node!.mqttConfig!.password { hasChanges = true } - } + if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true } } .onChange(of: root) { newRoot in - if node != nil && node?.mqttConfig != nil { - if newRoot != node!.mqttConfig!.root { hasChanges = true } - } + if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true } } .onChange(of: selectedTopic) { _, newSelectedTopic in root = newSelectedTopic @@ -320,10 +316,8 @@ struct MQTTConfig: View { .onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true } } - .onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in - if node != nil && node?.mqttConfig != nil { - if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true } - } + .onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in + if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true } } .onFirstAppear { // Need to request a MqttModuleConfig from the remote node before allowing changes diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index afef5727..4ba39834 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -155,45 +155,29 @@ struct TelemetryConfig: View { } } } - .onChange(of: deviceUpdateInterval) { newDeviceInterval in - if node != nil && node?.telemetryConfig != nil { - if newDeviceInterval != node!.telemetryConfig!.deviceUpdateInterval { hasChanges = true } - } + .onChange(of: deviceUpdateInterval) { _, newDeviceInterval in + if newDeviceInterval != node?.telemetryConfig?.deviceUpdateInterval ?? -1 { hasChanges = true } } - .onChange(of: environmentUpdateInterval) { newEnvInterval in - if node != nil && node?.telemetryConfig != nil { - if newEnvInterval != node!.telemetryConfig!.environmentUpdateInterval { hasChanges = true } - } + .onChange(of: environmentUpdateInterval) { _, newEnvInterval in + if newEnvInterval != node?.telemetryConfig?.environmentUpdateInterval ?? -1 { hasChanges = true } } - .onChange(of: environmentMeasurementEnabled) { newEnvEnabled in - if node != nil && node?.telemetryConfig != nil { - if newEnvEnabled != node!.telemetryConfig!.environmentMeasurementEnabled { hasChanges = true } - } + .onChange(of: environmentMeasurementEnabled) { _, newEnvEnabled in + if newEnvEnabled != node?.telemetryConfig?.environmentMeasurementEnabled { hasChanges = true } } - .onChange(of: environmentScreenEnabled) { newEnvScreenEnabled in - if node!.telemetryConfig != nil { - if newEnvScreenEnabled != node!.telemetryConfig!.environmentScreenEnabled { hasChanges = true } - } + .onChange(of: environmentScreenEnabled) { _, newEnvScreenEnabled in + if newEnvScreenEnabled != node?.telemetryConfig?.environmentScreenEnabled { hasChanges = true } } - .onChange(of: environmentDisplayFahrenheit) { newEnvDisplayF in - if node != nil && node?.telemetryConfig != nil { - if newEnvDisplayF != node!.telemetryConfig!.environmentDisplayFahrenheit { hasChanges = true } - } + .onChange(of: environmentDisplayFahrenheit) { _, newEnvDisplayF in + if newEnvDisplayF != node?.telemetryConfig?.environmentDisplayFahrenheit { hasChanges = true } } - .onChange(of: powerMeasurementEnabled) { newPowerMeasurementEnabled in - if node != nil && node?.telemetryConfig != nil { - if newPowerMeasurementEnabled != node!.telemetryConfig!.powerMeasurementEnabled { hasChanges = true } - } + .onChange(of: powerMeasurementEnabled) { _, newPowerMeasurementEnabled in + if newPowerMeasurementEnabled != node?.telemetryConfig?.powerMeasurementEnabled { hasChanges = true } } - .onChange(of: powerUpdateInterval) { newPowerUpdateInterval in - if node != nil && node?.telemetryConfig != nil { - if newPowerUpdateInterval != node!.telemetryConfig!.powerUpdateInterval { hasChanges = true } - } + .onChange(of: powerUpdateInterval) { _, newPowerUpdateInterval in + if newPowerUpdateInterval != node?.telemetryConfig?.powerUpdateInterval ?? -1 { hasChanges = true } } - .onChange(of: powerScreenEnabled) { newPowerScreenEnabled in - if node != nil && node?.telemetryConfig != nil { - if newPowerScreenEnabled != node!.telemetryConfig!.powerScreenEnabled { hasChanges = true } - } + .onChange(of: powerScreenEnabled) { _, newPowerScreenEnabled in + if newPowerScreenEnabled != node?.telemetryConfig?.powerScreenEnabled { hasChanges = true } } } } From 0cb4cc3714da843ee7ac2bce99c43dca83212630 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 6 Oct 2024 10:03:05 -0700 Subject: [PATCH 257/333] Hide signal meter on node details if hops away > 0 --- Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 69235f11..e0da484e 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -48,7 +48,7 @@ struct NodeInfoItem: View { color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75 ) - if node.snr != 0 && !node.viaMqtt { + if node.snr != 0 && !node.viaMqtt && node.hopsAway == 0 { Spacer() VStack { let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) From cba23fea6121b42ba8d5e3210e96d5eb7682b123 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 6 Oct 2024 15:00:21 -0700 Subject: [PATCH 258/333] Fix map zoom level --- Localizable.xcstrings | 28 +- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 7 +- Meshtastic/Views/Nodes/NodeMap.swift | 460 +++++++++--------- 3 files changed, 235 insertions(+), 260 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e284ac42..a1d2ddab 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1274,9 +1274,6 @@ } } } - }, - "Attribution:" : { - }, "automatic.detection" : { "extractionState" : "migrated", @@ -6819,9 +6816,6 @@ }, "Enable Notifications" : { - }, - "Enable Offline Maps" : { - }, "enabled" : { "localizations" : { @@ -11542,6 +11536,7 @@ } }, "map.recentering" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15870,9 +15865,6 @@ } } } - }, - "Offline Maps" : { - }, "OK" : { @@ -16712,9 +16704,6 @@ }, "PWD" : { - }, - "Radar" : { - }, "radio.configuration" : { "localizations" : { @@ -19855,9 +19844,6 @@ }, "Show Alerts" : { - }, - "Show Node History" : { - }, "Show nodes" : { @@ -19867,15 +19853,9 @@ }, "Show on the mesh map." : { - }, - "Show Route Lines" : { - }, "Show Waypoints " : { - }, - "Show Weather" : { - }, "Shut Down" : { @@ -21129,12 +21109,6 @@ }, "This will send a current position from your phone and enable fixed position." : { - }, - "Tile Server" : { - - }, - "Tiles above Labels" : { - }, "Time" : { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 1120e811..bec035b7 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -24,6 +24,7 @@ struct NodeMapSwiftUI: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic + @State var distance = 0.0 @State var scene: MKLookAroundScene? @State var isLookingAround = false @State var isShowingAltitude = false @@ -44,7 +45,7 @@ struct NodeMapSwiftUI: View { if node.hasPositions { ZStack { MapReader { _ in - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 3000, maximumDistance: .infinity), scope: mapScope) { + Map(position: $position, bounds: MapCameraBounds(minimumDistance: 0, maximumDistance: .infinity), scope: mapScope) { NodeMapContent(node: node) } .mapScope(mapScope) @@ -103,7 +104,7 @@ struct NodeMapSwiftUI: View { if node.positions?.count ?? 0 > 1 { position = .automatic } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) + position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: distance, heading: 0, pitch: 0)) } if let mostRecent { Task { @@ -128,7 +129,7 @@ struct NodeMapSwiftUI: View { position = .automatic } else { if let mrCoord = mostRecent?.coordinate { - position = .camera(MapCamera(centerCoordinate: mrCoord, distance: 8000, heading: 0, pitch: 60)) + position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0)) } } if self.scene == nil { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 9988f237..e81fc224 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -1,233 +1,233 @@ +//// +//// NodeMap.swift +//// MeshtasticApple +//// +//// Created by Garth Vander Houwen on 8/7/21. +//// // -// NodeMap.swift -// MeshtasticApple +//import SwiftUI +//import MapKit +//import CoreLocation +//import CoreData // -// Created by Garth Vander Houwen on 8/7/21. +//struct NodeMap: View { +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager // - -import SwiftUI -import MapKit -import CoreLocation -import CoreData - -struct NodeMap: View { - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - - @ObservedObject - var router: Router - @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer - @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering - @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines - @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins - @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps - @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer - @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer - @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer - @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels - let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], - predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) - private var positions: FetchedResults - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - predicate: NSPredicate( - format: "expire == nil || expire >= %@", Date() as NSDate - ), animation: .none) - private var waypoints: FetchedResults - @State var waypointCoordinate: WaypointCoordinate? - @State var selectedTracking: UserTrackingModes = .none - @State var isPresentingInfoSheet: Bool = false - @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( - mapName: "offlinemap", - tileType: "png", - canReplaceMapContent: true - ) - var body: some View { - NavigationStack { - ZStack { - MapViewSwiftUI( - onLongPress: { coord in - waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) - }, onWaypointEdit: { wpId in - if wpId > 0 { - waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) - } - }, - selectedMapLayer: selectedMapLayer, - positions: Array(positions), - waypoints: Array(waypoints), - userTrackingMode: selectedTracking.MKUserTrackingModeValue(), - showNodeHistory: enableMapNodeHistoryPins, - showRouteLines: enableMapRouteLines, - customMapOverlay: self.customMapOverlay - ) - VStack(alignment: .trailing) { - HStack(alignment: .top) { - Spacer() - MapButtons(tracking: $selectedTracking, isPresentingInfoSheet: $isPresentingInfoSheet) - .padding(.trailing, 8) - .padding(.top, 16) - } - Spacer() - TileDownloadStatus() - .padding(.trailing, 16) - .padding(.bottom, 20) - } - } - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) - .frame(maxHeight: .infinity) - .sheet(item: $waypointCoordinate, content: { wpc in - WaypointFormMapKit(coordinate: wpc) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - }) - .sheet(isPresented: $isPresentingInfoSheet) { - VStack { - Form { - Section(header: Text("Map Options")) { - Picker(selection: $selectedMapLayer, label: Text("")) { - ForEach(MapLayer.allCases, id: \.self) { layer in - if layer == MapLayer.offline && enableOfflineMaps { - Text(layer.localized) - } else if layer != MapLayer.offline { - Text(layer.localized) - } - } - } - .pickerStyle(SegmentedPickerStyle()) - .onChange(of: selectedMapLayer) { _, newMapLayer in - UserDefaults.mapLayer = newMapLayer - } - .padding(.top, 5) - .padding(.bottom, 5) - Toggle(isOn: $enableMapRecentering) { - Label("map.recentering", systemImage: "camera.metering.center.weighted") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.enableMapRecentering.toggle() - UserDefaults.enableMapRecentering = self.enableMapRecentering - } - Toggle(isOn: $enableMapNodeHistoryPins) { - Label("Show Node History", systemImage: "building.columns.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.enableMapNodeHistoryPins.toggle() - UserDefaults.enableMapNodeHistoryPins = self.enableMapNodeHistoryPins - } - Toggle(isOn: $enableMapRouteLines) { - Label("Show Route Lines", systemImage: "road.lanes") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.enableMapRouteLines.toggle() - UserDefaults.enableMapRouteLines = self.enableMapRouteLines - } - let locale = Locale.current - if locale.region?.identifier ?? "no locale" == "US" { - Toggle(isOn: $enableOverlayServer) { - Label("Show Weather", systemImage: "cloud.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.enableOverlayServer.toggle() - UserDefaults.enableOverlayServer = self.enableOverlayServer - } - if enableOverlayServer { - Picker(selection: $selectedOverlayServer, - label: Text("Radar")) { - ForEach(MapOverlayServer.allCases, id: \.self) { mos in - Text(mos.description) - .font(.footnote) - } - } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in - UserDefaults.mapOverlayServer = newSelectedOverlayServer - } - Text(LocalizedStringKey(selectedOverlayServer.attribution)) - .font(.footnote) - .foregroundColor(.gray) - .padding(0) - } - } - } - Section(header: Text("Offline Maps")) { - Toggle(isOn: $enableOfflineMaps) { - Text("Enable Offline Maps") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in - UserDefaults.enableOfflineMaps = newEnableOfflineMaps - if !enableOfflineMaps { - if self.selectedMapLayer == .offline { - self.selectedMapLayer = .standard - } - } - } - if enableOfflineMaps { - VStack(alignment: .leading) { - Picker(selection: $selectedTileServer, - label: Text("Tile Server")) { - ForEach(MapTileServer.allCases, id: \.self) { tsl in - Text(tsl.description) - } - } - .pickerStyle(DefaultPickerStyle()) - .onChange(of: (selectedTileServer)) { _, newSelectedTileServer in - UserDefaults.mapTileServer = newSelectedTileServer - } - Text("Attribution:") - .fontWeight(.semibold) - .font(.footnote) - Text(LocalizedStringKey(selectedTileServer.attribution)) - .font(.footnote) - .foregroundColor(.gray) - .padding(0) - Divider() - Toggle(isOn: $mapTilesAboveLabels) { - Text("Tiles above Labels") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.mapTilesAboveLabels.toggle() - UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels - } - } - } - } - } - #if targetEnvironment(macCatalyst) - Button { - isPresentingInfoSheet = false - } label: { - Label("close", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - #endif - } - .presentationDetents([enableOfflineMaps || enableOverlayServer ? .large : .medium]) - .presentationDragIndicator(.visible) - } - } - .navigationBarItems(leading: - MeshtasticLogo(), trailing: - ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : - "?") - }) - .onAppear(perform: { - UIApplication.shared.isIdleTimerDisabled = true - }) - .onDisappear(perform: { - UIApplication.shared.isIdleTimerDisabled = false - }) - } -} +// @ObservedObject +// var router: Router +// @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer +// @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering +// @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines +// @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins +// @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps +// @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer +// @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer +// @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer +// @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels +// let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate +// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], +// predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) +// private var positions: FetchedResults +// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], +// predicate: NSPredicate( +// format: "expire == nil || expire >= %@", Date() as NSDate +// ), animation: .none) +// private var waypoints: FetchedResults +// @State var waypointCoordinate: WaypointCoordinate? +// @State var selectedTracking: UserTrackingModes = .none +// @State var isPresentingInfoSheet: Bool = false +// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( +// mapName: "offlinemap", +// tileType: "png", +// canReplaceMapContent: true +// ) +// var body: some View { +// NavigationStack { +// ZStack { +// MapViewSwiftUI( +// onLongPress: { coord in +// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) +// }, onWaypointEdit: { wpId in +// if wpId > 0 { +// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) +// } +// }, +// selectedMapLayer: selectedMapLayer, +// positions: Array(positions), +// waypoints: Array(waypoints), +// userTrackingMode: selectedTracking.MKUserTrackingModeValue(), +// showNodeHistory: enableMapNodeHistoryPins, +// showRouteLines: enableMapRouteLines, +// customMapOverlay: self.customMapOverlay +// ) +// VStack(alignment: .trailing) { +// HStack(alignment: .top) { +// Spacer() +// MapButtons(tracking: $selectedTracking, isPresentingInfoSheet: $isPresentingInfoSheet) +// .padding(.trailing, 8) +// .padding(.top, 16) +// } +// Spacer() +// TileDownloadStatus() +// .padding(.trailing, 16) +// .padding(.bottom, 20) +// } +// } +// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) +// .frame(maxHeight: .infinity) +// .sheet(item: $waypointCoordinate, content: { wpc in +// WaypointFormMapKit(coordinate: wpc) +// .presentationDetents([.medium, .large]) +// .presentationDragIndicator(.automatic) +// }) +// .sheet(isPresented: $isPresentingInfoSheet) { +// VStack { +// Form { +// Section(header: Text("Map Options")) { +// Picker(selection: $selectedMapLayer, label: Text("")) { +// ForEach(MapLayer.allCases, id: \.self) { layer in +// if layer == MapLayer.offline && enableOfflineMaps { +// Text(layer.localized) +// } else if layer != MapLayer.offline { +// Text(layer.localized) +// } +// } +// } +// .pickerStyle(SegmentedPickerStyle()) +// .onChange(of: selectedMapLayer) { _, newMapLayer in +// UserDefaults.mapLayer = newMapLayer +// } +// .padding(.top, 5) +// .padding(.bottom, 5) +// Toggle(isOn: $enableMapRecentering) { +// Label("map.recentering", systemImage: "camera.metering.center.weighted") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// .onTapGesture { +// self.enableMapRecentering.toggle() +// UserDefaults.enableMapRecentering = self.enableMapRecentering +// } +// Toggle(isOn: $enableMapNodeHistoryPins) { +// Label("Show Node History", systemImage: "building.columns.fill") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// .onTapGesture { +// self.enableMapNodeHistoryPins.toggle() +// UserDefaults.enableMapNodeHistoryPins = self.enableMapNodeHistoryPins +// } +// Toggle(isOn: $enableMapRouteLines) { +// Label("Show Route Lines", systemImage: "road.lanes") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// .onTapGesture { +// self.enableMapRouteLines.toggle() +// UserDefaults.enableMapRouteLines = self.enableMapRouteLines +// } +// let locale = Locale.current +// if locale.region?.identifier ?? "no locale" == "US" { +// Toggle(isOn: $enableOverlayServer) { +// Label("Show Weather", systemImage: "cloud.fill") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// .onTapGesture { +// self.enableOverlayServer.toggle() +// UserDefaults.enableOverlayServer = self.enableOverlayServer +// } +// if enableOverlayServer { +// Picker(selection: $selectedOverlayServer, +// label: Text("Radar")) { +// ForEach(MapOverlayServer.allCases, id: \.self) { mos in +// Text(mos.description) +// .font(.footnote) +// } +// } +// .pickerStyle(DefaultPickerStyle()) +// .onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in +// UserDefaults.mapOverlayServer = newSelectedOverlayServer +// } +// Text(LocalizedStringKey(selectedOverlayServer.attribution)) +// .font(.footnote) +// .foregroundColor(.gray) +// .padding(0) +// } +// } +// } +// Section(header: Text("Offline Maps")) { +// Toggle(isOn: $enableOfflineMaps) { +// Text("Enable Offline Maps") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// .onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in +// UserDefaults.enableOfflineMaps = newEnableOfflineMaps +// if !enableOfflineMaps { +// if self.selectedMapLayer == .offline { +// self.selectedMapLayer = .standard +// } +// } +// } +// if enableOfflineMaps { +// VStack(alignment: .leading) { +// Picker(selection: $selectedTileServer, +// label: Text("Tile Server")) { +// ForEach(MapTileServer.allCases, id: \.self) { tsl in +// Text(tsl.description) +// } +// } +// .pickerStyle(DefaultPickerStyle()) +// .onChange(of: (selectedTileServer)) { _, newSelectedTileServer in +// UserDefaults.mapTileServer = newSelectedTileServer +// } +// Text("Attribution:") +// .fontWeight(.semibold) +// .font(.footnote) +// Text(LocalizedStringKey(selectedTileServer.attribution)) +// .font(.footnote) +// .foregroundColor(.gray) +// .padding(0) +// Divider() +// Toggle(isOn: $mapTilesAboveLabels) { +// Text("Tiles above Labels") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// .onTapGesture { +// self.mapTilesAboveLabels.toggle() +// UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels +// } +// } +// } +// } +// } +// #if targetEnvironment(macCatalyst) +// Button { +// isPresentingInfoSheet = false +// } label: { +// Label("close", systemImage: "xmark") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// #endif +// } +// .presentationDetents([enableOfflineMaps || enableOverlayServer ? .large : .medium]) +// .presentationDragIndicator(.visible) +// } +// } +// .navigationBarItems(leading: +// MeshtasticLogo(), trailing: +// ZStack { +// ConnectedDevice( +// bluetoothOn: bleManager.isSwitchedOn, +// deviceConnected: bleManager.connectedPeripheral != nil, +// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : +// "?") +// }) +// .onAppear(perform: { +// UIApplication.shared.isIdleTimerDisabled = true +// }) +// .onDisappear(perform: { +// UIApplication.shared.isIdleTimerDisabled = false +// }) +// } +//} From e7fad8e57379769508c94e485f98d8ec4de09d02 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Oct 2024 19:27:23 -0700 Subject: [PATCH 259/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 20a5586d..c6706b44 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1699,7 +1699,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1733,7 +1733,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1765,7 +1765,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1798,7 +1798,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 4b44224fb2f489ffbf106d318305c00c1142cb91 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 9 Oct 2024 19:32:32 -0700 Subject: [PATCH 260/333] Bump protos and device hardware --- Meshtastic/Resources/DeviceHardware.json | 16 ++ .../Sources/meshtastic/atak.pb.swift | 4 +- .../Sources/meshtastic/config.pb.swift | 24 +++ .../Sources/meshtastic/mesh.pb.swift | 49 ++++- .../Sources/meshtastic/module_config.pb.swift | 143 ++++++++++++-- .../Sources/meshtastic/telemetry.pb.swift | 183 +++++++++++++++++- protobufs | 2 +- 7 files changed, 398 insertions(+), 23 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index eaf5db0b..0b88202d 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -127,6 +127,14 @@ "activelySupported": true, "displayName": "LILYGO T-LoRa T3-S3" }, + { + "hwModel": 16, + "hwModelSlug": "TLORA_T3_S3", + "platformioTarget": "tlora-t3s3-epaper", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "LILYGO T-LoRa T3-S3 E-Paper" + }, { "hwModel": 17, "hwModelSlug": "NANO_G1_EXPLORER", @@ -414,5 +422,13 @@ "architecture": "nrf52840", "activelySupported": true, "displayName": "Seeed Card Tracker T1000-E" + }, + { + "hwModel": 72, + "hwModelSlug": "Seeed_XIAO_S3", + "platformioTarget": "seeed-xiao-s3", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Seeed XIAO S3" } ] diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index c756d94d..867648a9 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -324,7 +324,7 @@ public struct TAKPacket { /// /// Generic CoT detail XML - /// May be compressed / truncated by the sender + /// May be compressed / truncated by the sender (EUD) public var detail: Data { get { if case .detail(let v)? = payloadVariant {return v} @@ -346,7 +346,7 @@ public struct TAKPacket { case chat(GeoChat) /// /// Generic CoT detail XML - /// May be compressed / truncated by the sender + /// May be compressed / truncated by the sender (EUD) case detail(Data) #if !swift(>=4.1) diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 37832baa..38df8ea0 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1344,6 +1344,18 @@ public struct Config { /// /// Singapore 923mhz case sg923 // = 18 + + /// + /// Philippines 433mhz + case ph433 // = 19 + + /// + /// Philippines 868mhz + case ph868 // = 20 + + /// + /// Philippines 915mhz + case ph915 // = 21 case UNRECOGNIZED(Int) public init() { @@ -1371,6 +1383,9 @@ public struct Config { case 16: self = .my433 case 17: self = .my919 case 18: self = .sg923 + case 19: self = .ph433 + case 20: self = .ph868 + case 21: self = .ph915 default: self = .UNRECOGNIZED(rawValue) } } @@ -1396,6 +1411,9 @@ public struct Config { case .my433: return 16 case .my919: return 17 case .sg923: return 18 + case .ph433: return 19 + case .ph868: return 20 + case .ph915: return 21 case .UNRECOGNIZED(let i): return i } } @@ -1747,6 +1765,9 @@ extension Config.LoRaConfig.RegionCode: CaseIterable { .my433, .my919, .sg923, + .ph433, + .ph868, + .ph915, ] } @@ -2834,6 +2855,9 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { 16: .same(proto: "MY_433"), 17: .same(proto: "MY_919"), 18: .same(proto: "SG_923"), + 19: .same(proto: "PH_433"), + 20: .same(proto: "PH_868"), + 21: .same(proto: "PH_915"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 6bddec60..f604d4a7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -126,6 +126,10 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Heltec HRU-3601: https://heltec.org/project/hru-3601/ case heltecHru3601 // = 23 + /// + /// Heltec Wireless Bridge + case heltecWirelessBridge // = 24 + /// /// B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station case stationG1 // = 25 @@ -197,7 +201,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case drDev // = 41 /// - /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ case m5Stack // = 42 /// @@ -355,10 +359,27 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// ^^^ short A0 to switch to I2C address 0x3C case rp2040FeatherRfm95 // = 76 - /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ case m5StackCorebasic // = 77 case m5StackCore2 // = 78 + /// Pico2 with Waveshare Hat, same as Pico + case rpiPico2 // = 79 + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ + case m5StackCores3 // = 80 + + /// Seeed XIAO S3 DK + case seeedXiaoS3 // = 81 + + /// + /// Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1 + case ms24Sf1 // = 82 + + /// + /// Lilygo TLora-C6 with the new ESP32-C6 MCU + case tloraC6 // = 83 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -396,6 +417,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 21: self = .wioWm1110 case 22: self = .rak2560 case 23: self = .heltecHru3601 + case 24: self = .heltecWirelessBridge case 25: self = .stationG1 case 26: self = .rak11310 case 27: self = .senseloraRp2040 @@ -450,6 +472,11 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 76: self = .rp2040FeatherRfm95 case 77: self = .m5StackCorebasic case 78: self = .m5StackCore2 + case 79: self = .rpiPico2 + case 80: self = .m5StackCores3 + case 81: self = .seeedXiaoS3 + case 82: self = .ms24Sf1 + case 83: self = .tloraC6 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -481,6 +508,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .wioWm1110: return 21 case .rak2560: return 22 case .heltecHru3601: return 23 + case .heltecWirelessBridge: return 24 case .stationG1: return 25 case .rak11310: return 26 case .senseloraRp2040: return 27 @@ -535,6 +563,11 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .rp2040FeatherRfm95: return 76 case .m5StackCorebasic: return 77 case .m5StackCore2: return 78 + case .rpiPico2: return 79 + case .m5StackCores3: return 80 + case .seeedXiaoS3: return 81 + case .ms24Sf1: return 82 + case .tloraC6: return 83 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -571,6 +604,7 @@ extension HardwareModel: CaseIterable { .wioWm1110, .rak2560, .heltecHru3601, + .heltecWirelessBridge, .stationG1, .rak11310, .senseloraRp2040, @@ -625,6 +659,11 @@ extension HardwareModel: CaseIterable { .rp2040FeatherRfm95, .m5StackCorebasic, .m5StackCore2, + .rpiPico2, + .m5StackCores3, + .seeedXiaoS3, + .ms24Sf1, + .tloraC6, .privateHw, ] } @@ -3262,6 +3301,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 21: .same(proto: "WIO_WM1110"), 22: .same(proto: "RAK2560"), 23: .same(proto: "HELTEC_HRU_3601"), + 24: .same(proto: "HELTEC_WIRELESS_BRIDGE"), 25: .same(proto: "STATION_G1"), 26: .same(proto: "RAK11310"), 27: .same(proto: "SENSELORA_RP2040"), @@ -3316,6 +3356,11 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 76: .same(proto: "RP2040_FEATHER_RFM95"), 77: .same(proto: "M5STACK_COREBASIC"), 78: .same(proto: "M5STACK_CORE2"), + 79: .same(proto: "RPI_PICO2"), + 80: .same(proto: "M5STACK_CORES3"), + 81: .same(proto: "SEEED_XIAO_S3"), + 82: .same(proto: "MS24SF1"), + 83: .same(proto: "TLORA_C6"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 3186c349..30a8e0a4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -475,13 +475,15 @@ public struct ModuleConfig { public var enabled: Bool = false /// - /// Interval in seconds of how often we can send a message to the mesh when a state change is detected + /// Interval in seconds of how often we can send a message to the mesh when a + /// trigger event is detected public var minimumBroadcastSecs: UInt32 = 0 /// - /// Interval in seconds of how often we should send a message to the mesh with the current state regardless of changes - /// When set to 0, only state changes will be broadcasted - /// Works as a sort of status heartbeat for peace of mind + /// Interval in seconds of how often we should send a message to the mesh + /// with the current state regardless of trigger events When set to 0, only + /// trigger events will be broadcasted Works as a sort of status heartbeat + /// for peace of mind public var stateBroadcastSecs: UInt32 = 0 /// @@ -500,9 +502,8 @@ public struct ModuleConfig { public var monitorPin: UInt32 = 0 /// - /// Whether or not the GPIO pin state detection is triggered on HIGH (1) - /// Otherwise LOW (0) - public var detectionTriggeredHigh: Bool = false + /// The type of trigger event to be used + public var detectionTriggerType: ModuleConfig.DetectionSensorConfig.TriggerType = .logicLow /// /// Whether or not use INPUT_PULLUP mode for GPIO pin @@ -511,6 +512,60 @@ public struct ModuleConfig { public var unknownFields = SwiftProtobuf.UnknownStorage() + public enum TriggerType: SwiftProtobuf.Enum { + public typealias RawValue = Int + + /// Event is triggered if pin is low + case logicLow // = 0 + + /// Event is triggered if pin is high + case logicHigh // = 1 + + /// Event is triggered when pin goes high to low + case fallingEdge // = 2 + + /// Event is triggered when pin goes low to high + case risingEdge // = 3 + + /// Event is triggered on every pin state change, low is considered to be + /// "active" + case eitherEdgeActiveLow // = 4 + + /// Event is triggered on every pin state change, high is considered to be + /// "active" + case eitherEdgeActiveHigh // = 5 + case UNRECOGNIZED(Int) + + public init() { + self = .logicLow + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .logicLow + case 1: self = .logicHigh + case 2: self = .fallingEdge + case 3: self = .risingEdge + case 4: self = .eitherEdgeActiveLow + case 5: self = .eitherEdgeActiveHigh + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .logicLow: return 0 + case .logicHigh: return 1 + case .fallingEdge: return 2 + case .risingEdge: return 3 + case .eitherEdgeActiveLow: return 4 + case .eitherEdgeActiveHigh: return 5 + case .UNRECOGNIZED(let i): return i + } + } + + } + public init() {} } @@ -980,20 +1035,32 @@ public struct ModuleConfig { public var airQualityInterval: UInt32 = 0 /// - /// Interval in seconds of how often we should try to send our - /// air quality metrics to the mesh + /// Enable/disable Power metrics public var powerMeasurementEnabled: Bool = false /// /// Interval in seconds of how often we should try to send our - /// air quality metrics to the mesh + /// power metrics to the mesh public var powerUpdateInterval: UInt32 = 0 /// - /// Interval in seconds of how often we should try to send our - /// air quality metrics to the mesh + /// Enable/Disable the power measurement module on-device display public var powerScreenEnabled: Bool = false + /// + /// Preferences for the (Health) Telemetry Module + /// Enable/Disable the telemetry measurement module measurement collection + public var healthMeasurementEnabled: Bool = false + + /// + /// Interval in seconds of how often we should try to send our + /// health metrics to the mesh + public var healthUpdateInterval: UInt32 = 0 + + /// + /// Enable/Disable the health telemetry module on-device display + public var healthScreenEnabled: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1167,6 +1234,18 @@ public struct ModuleConfig { #if swift(>=4.2) +extension ModuleConfig.DetectionSensorConfig.TriggerType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ + .logicLow, + .logicHigh, + .fallingEdge, + .risingEdge, + .eitherEdgeActiveLow, + .eitherEdgeActiveHigh, + ] +} + extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ @@ -1266,6 +1345,7 @@ extension ModuleConfig.MapReportSettings: @unchecked Sendable {} extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} +extension ModuleConfig.DetectionSensorConfig.TriggerType: @unchecked Sendable {} extension ModuleConfig.AudioConfig: @unchecked Sendable {} extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} @@ -1787,7 +1867,7 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob 4: .standard(proto: "send_bell"), 5: .same(proto: "name"), 6: .standard(proto: "monitor_pin"), - 7: .standard(proto: "detection_triggered_high"), + 7: .standard(proto: "detection_trigger_type"), 8: .standard(proto: "use_pullup"), ] @@ -1803,7 +1883,7 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob case 4: try { try decoder.decodeSingularBoolField(value: &self.sendBell) }() case 5: try { try decoder.decodeSingularStringField(value: &self.name) }() case 6: try { try decoder.decodeSingularUInt32Field(value: &self.monitorPin) }() - case 7: try { try decoder.decodeSingularBoolField(value: &self.detectionTriggeredHigh) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.detectionTriggerType) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.usePullup) }() default: break } @@ -1829,8 +1909,8 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob if self.monitorPin != 0 { try visitor.visitSingularUInt32Field(value: self.monitorPin, fieldNumber: 6) } - if self.detectionTriggeredHigh != false { - try visitor.visitSingularBoolField(value: self.detectionTriggeredHigh, fieldNumber: 7) + if self.detectionTriggerType != .logicLow { + try visitor.visitSingularEnumField(value: self.detectionTriggerType, fieldNumber: 7) } if self.usePullup != false { try visitor.visitSingularBoolField(value: self.usePullup, fieldNumber: 8) @@ -1845,13 +1925,24 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob if lhs.sendBell != rhs.sendBell {return false} if lhs.name != rhs.name {return false} if lhs.monitorPin != rhs.monitorPin {return false} - if lhs.detectionTriggeredHigh != rhs.detectionTriggeredHigh {return false} + if lhs.detectionTriggerType != rhs.detectionTriggerType {return false} if lhs.usePullup != rhs.usePullup {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } +extension ModuleConfig.DetectionSensorConfig.TriggerType: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "LOGIC_LOW"), + 1: .same(proto: "LOGIC_HIGH"), + 2: .same(proto: "FALLING_EDGE"), + 3: .same(proto: "RISING_EDGE"), + 4: .same(proto: "EITHER_EDGE_ACTIVE_LOW"), + 5: .same(proto: "EITHER_EDGE_ACTIVE_HIGH"), + ] +} + extension ModuleConfig.AudioConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".AudioConfig" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -2326,6 +2417,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me 8: .standard(proto: "power_measurement_enabled"), 9: .standard(proto: "power_update_interval"), 10: .standard(proto: "power_screen_enabled"), + 11: .standard(proto: "health_measurement_enabled"), + 12: .standard(proto: "health_update_interval"), + 13: .standard(proto: "health_screen_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2344,6 +2438,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me case 8: try { try decoder.decodeSingularBoolField(value: &self.powerMeasurementEnabled) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.powerUpdateInterval) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.powerScreenEnabled) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.healthMeasurementEnabled) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.healthUpdateInterval) }() + case 13: try { try decoder.decodeSingularBoolField(value: &self.healthScreenEnabled) }() default: break } } @@ -2380,6 +2477,15 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me if self.powerScreenEnabled != false { try visitor.visitSingularBoolField(value: self.powerScreenEnabled, fieldNumber: 10) } + if self.healthMeasurementEnabled != false { + try visitor.visitSingularBoolField(value: self.healthMeasurementEnabled, fieldNumber: 11) + } + if self.healthUpdateInterval != 0 { + try visitor.visitSingularUInt32Field(value: self.healthUpdateInterval, fieldNumber: 12) + } + if self.healthScreenEnabled != false { + try visitor.visitSingularBoolField(value: self.healthScreenEnabled, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -2394,6 +2500,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me if lhs.powerMeasurementEnabled != rhs.powerMeasurementEnabled {return false} if lhs.powerUpdateInterval != rhs.powerUpdateInterval {return false} if lhs.powerScreenEnabled != rhs.powerScreenEnabled {return false} + if lhs.healthMeasurementEnabled != rhs.healthMeasurementEnabled {return false} + if lhs.healthUpdateInterval != rhs.healthUpdateInterval {return false} + if lhs.healthScreenEnabled != rhs.healthScreenEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index dc1d6cce..ec5faaa4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -144,6 +144,14 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor case customSensor // = 29 + + /// + /// MAX30102 Pulse Oximeter and Heart-Rate Sensor + case max30102 // = 30 + + /// + /// MLX90614 non-contact IR temperature sensor. + case mlx90614 // = 31 case UNRECOGNIZED(Int) public init() { @@ -182,6 +190,8 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 27: self = .icm20948 case 28: self = .max17048 case 29: self = .customSensor + case 30: self = .max30102 + case 31: self = .mlx90614 default: self = .UNRECOGNIZED(rawValue) } } @@ -218,6 +228,8 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .icm20948: return 27 case .max17048: return 28 case .customSensor: return 29 + case .max30102: return 30 + case .mlx90614: return 31 case .UNRECOGNIZED(let i): return i } } @@ -259,6 +271,8 @@ extension TelemetrySensorType: CaseIterable { .icm20948, .max17048, .customSensor, + .max30102, + .mlx90614, ] } @@ -806,7 +820,7 @@ public struct LocalStats { public var numPacketsTx: UInt32 = 0 /// - /// Number of packets received good + /// Number of packets received (both good and bad) public var numPacketsRx: UInt32 = 0 /// @@ -821,11 +835,74 @@ public struct LocalStats { /// Number of nodes total public var numTotalNodes: UInt32 = 0 + /// + /// Number of received packets that were duplicates (due to multiple nodes relaying). + /// If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role. + public var numRxDupe: UInt32 = 0 + + /// + /// Number of packets we transmitted that were a relay for others (not originating from ourselves). + public var numTxRelay: UInt32 = 0 + + /// + /// Number of times we canceled a packet to be relayed, because someone else did it before us. + /// 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 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} } +/// +/// Health telemetry metrics +public struct HealthMetrics { + // 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. + + /// + /// Heart rate (beats per minute) + public var heartBpm: UInt32 { + get {return _heartBpm ?? 0} + set {_heartBpm = newValue} + } + /// Returns true if `heartBpm` has been explicitly set. + public var hasHeartBpm: Bool {return self._heartBpm != nil} + /// Clears the value of `heartBpm`. Subsequent reads from it will return its default value. + public mutating func clearHeartBpm() {self._heartBpm = nil} + + /// + /// SpO2 (blood oxygen saturation) level + public var spO2: UInt32 { + get {return _spO2 ?? 0} + set {_spO2 = newValue} + } + /// Returns true if `spO2` has been explicitly set. + public var hasSpO2: Bool {return self._spO2 != nil} + /// Clears the value of `spO2`. Subsequent reads from it will return its default value. + public mutating func clearSpO2() {self._spO2 = nil} + + /// + /// Body temperature in degrees Celsius + public var temperature: Float { + get {return _temperature ?? 0} + set {_temperature = newValue} + } + /// Returns true if `temperature` has been explicitly set. + public var hasTemperature: Bool {return self._temperature != nil} + /// Clears the value of `temperature`. Subsequent reads from it will return its default value. + public mutating func clearTemperature() {self._temperature = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _heartBpm: UInt32? = nil + fileprivate var _spO2: UInt32? = nil + fileprivate var _temperature: Float? = nil +} + /// /// Types of Measurements the telemetry module is equipped to handle public struct Telemetry { @@ -889,6 +966,16 @@ public struct Telemetry { set {variant = .localStats(newValue)} } + /// + /// Health telemetry metrics + public var healthMetrics: HealthMetrics { + get { + if case .healthMetrics(let v)? = variant {return v} + return HealthMetrics() + } + set {variant = .healthMetrics(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Variant: Equatable { @@ -907,6 +994,9 @@ public struct Telemetry { /// /// Local device mesh statistics case localStats(LocalStats) + /// + /// Health telemetry metrics + case healthMetrics(HealthMetrics) #if !swift(>=4.1) public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { @@ -934,6 +1024,10 @@ public struct Telemetry { guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } return l == r }() + case (.healthMetrics, .healthMetrics): return { + guard case .healthMetrics(let l) = lhs, case .healthMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -970,6 +1064,7 @@ extension EnvironmentMetrics: @unchecked Sendable {} extension PowerMetrics: @unchecked Sendable {} extension AirQualityMetrics: @unchecked Sendable {} extension LocalStats: @unchecked Sendable {} +extension HealthMetrics: @unchecked Sendable {} extension Telemetry: @unchecked Sendable {} extension Telemetry.OneOf_Variant: @unchecked Sendable {} extension Nau7802Config: @unchecked Sendable {} @@ -1011,6 +1106,8 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 27: .same(proto: "ICM20948"), 28: .same(proto: "MAX17048"), 29: .same(proto: "CUSTOM_SENSOR"), + 30: .same(proto: "MAX30102"), + 31: .same(proto: "MLX90614"), ] } @@ -1457,6 +1554,9 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 6: .standard(proto: "num_packets_rx_bad"), 7: .standard(proto: "num_online_nodes"), 8: .standard(proto: "num_total_nodes"), + 9: .standard(proto: "num_rx_dupe"), + 10: .standard(proto: "num_tx_relay"), + 11: .standard(proto: "num_tx_relay_canceled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1473,6 +1573,9 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 6: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsRxBad) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineNodes) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.numTotalNodes) }() + 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) }() default: break } } @@ -1503,6 +1606,15 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.numTotalNodes != 0 { try visitor.visitSingularUInt32Field(value: self.numTotalNodes, fieldNumber: 8) } + if self.numRxDupe != 0 { + try visitor.visitSingularUInt32Field(value: self.numRxDupe, fieldNumber: 9) + } + if self.numTxRelay != 0 { + try visitor.visitSingularUInt32Field(value: self.numTxRelay, fieldNumber: 10) + } + if self.numTxRelayCanceled != 0 { + try visitor.visitSingularUInt32Field(value: self.numTxRelayCanceled, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -1515,6 +1627,57 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.numPacketsRxBad != rhs.numPacketsRxBad {return false} if lhs.numOnlineNodes != rhs.numOnlineNodes {return false} if lhs.numTotalNodes != rhs.numTotalNodes {return false} + if lhs.numRxDupe != rhs.numRxDupe {return false} + if lhs.numTxRelay != rhs.numTxRelay {return false} + if lhs.numTxRelayCanceled != rhs.numTxRelayCanceled {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension HealthMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HealthMetrics" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "heart_bpm"), + 2: .same(proto: "spO2"), + 3: .same(proto: "temperature"), + ] + + 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._heartBpm) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self._spO2) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self._temperature) }() + 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 + try { if let v = self._heartBpm { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._spO2 { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._temperature { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: HealthMetrics, rhs: HealthMetrics) -> Bool { + if lhs._heartBpm != rhs._heartBpm {return false} + if lhs._spO2 != rhs._spO2 {return false} + if lhs._temperature != rhs._temperature {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1529,6 +1692,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 4: .standard(proto: "air_quality_metrics"), 5: .standard(proto: "power_metrics"), 6: .standard(proto: "local_stats"), + 7: .standard(proto: "health_metrics"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1603,6 +1767,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .localStats(v) } }() + case 7: try { + var v: HealthMetrics? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .healthMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .healthMetrics(v) + } + }() default: break } } @@ -1637,6 +1814,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .localStats(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 6) }() + case .healthMetrics?: try { + guard case .healthMetrics(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/protobufs b/protobufs index 5709c0a0..c9ae7fd4 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5709c0a05eaefccbc9cb8ed3917adbf5fd134197 +Subproject commit c9ae7fd478bffe5f954b30de6cb140821fe9ff52 From 4508d12bbabae9c01292fca0e55164582fb66922 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Wed, 9 Oct 2024 22:53:29 -0500 Subject: [PATCH 261/333] Add missing timestamps to TraceRoute data structures --- Meshtastic/Helpers/BLEManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 805f32a2..e39419af 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -858,6 +858,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) + traceRouteHop.time = Date() if routingMessage.snrTowards.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) } @@ -910,6 +911,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) + traceRoute?.time = Date() do { try context.save() Logger.data.info("💾 Saved Trace Route") From fa4e000cebbe19c8f26f04664ee3b71310d36d44 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 10 Oct 2024 06:13:14 -0700 Subject: [PATCH 262/333] Updates for new protos --- Localizable.xcstrings | 68 +-- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Enums/LoraConfigEnums.swift | 57 ++- Meshtastic/Helpers/MeshPackets.swift | 3 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 482 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 4 +- Meshtastic/Views/Bluetooth/Connect.swift | 3 + Meshtastic/Views/Helpers/SecureInput.swift | 2 +- .../Config/Module/DetectionSensorConfig.swift | 16 +- .../Settings/Config/SecurityConfig.swift | 66 ++- Widgets/MeshActivityAttributes.swift | 5 + Widgets/WidgetsLiveActivity.swift | 2 +- 13 files changed, 646 insertions(+), 68 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a1d2ddab..da00af42 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -469,9 +469,6 @@ }, "Admin & Direct Message Keys" : { - }, - "Admin Key" : { - }, "admin.log" : { "extractionState" : "manual", @@ -1534,13 +1531,13 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。" + "value" : "尝试连接%d失败,你可能需要在系统设置的蓝牙选项中忽略该电台。" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。" + "value" : "嘗試連接%d失敗,你可能需要在系统設定的藍芽選項中忽略該電台。" } } } @@ -5198,9 +5195,6 @@ }, "Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge." : { - }, - "Detection trigger High" : { - }, "detection.sensor" : { "localizations" : { @@ -5877,55 +5871,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list." + "value" : "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Relays messages with minimal overhead. Not visible in Nodes list." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages avec un minimum de surcharge. Invisible dans la liste des noeuds." } }, "he" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "מכשיר תשתית להרחבת המש על ידי העברת הודעות עם דאטה נוסף מינימלי." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Przekaźnik - Pakiety siatki będą preferować trasowanie przez ten węzeł. Ta rola eliminuje niepotrzebny nadmiar, taki jak NodeInfo, DeviceTelemetry i inne pakiety siatki, skutkując tym, że urządzenie nie będzie widoczne jako część sieci. Proszę zobaczyć tryb Rebroadcast dla dodatkowych ustawień specyficznych dla tej roli." } }, "pt-PT" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens com sobrecarga mínima. Não visível na lista de Nós." } }, "se" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Infrastrukturnod för att utöka nätverkstäckningen genom att vidarebefordra meddelanden med minimal overhead. Syns inte i Noder-listan." } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如节点信息、设备遥测和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。" } }, "zh-Hant-TW" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。" } } @@ -5936,55 +5930,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list." + "value" : "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Needs exceptional coverage. Visible in Nodes list." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages. Visible dans la liste des noeuds." } }, "he" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "מכשיר תשתית להרחבת המש על ידי העברת הודעות. מופיע ברשימת מכשירים." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Router - Pakiety siatki będą preferować trasowanie przez ten węzeł. Zakłada, że urządzenie będzie działać samodzielnie, umieszczone w miejscu z przewagą zasięgu. UWAGA: Radia BLE/Wi-Fi i ekran OLED zostaną uśpione." } }, "pt-PT" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens. Visível na lista de Nós." } }, "se" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Infrastrukturnod för att utöka nätverkstäckningen genom att vidarebefordra meddelanden. Synlig i Noder-listan." } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "纯路由模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。" } }, "zh-Hant-TW" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。" } } @@ -6002,7 +5996,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Combination of both ROUTER and CLIENT. Not for mobile devices. Deprecated" + "value" : "Deprecated role use client." } }, "fr" : { @@ -16683,6 +16677,9 @@ }, "Primary" : { + }, + "Primary Admin Key" : { + }, "Primary GPIO" : { @@ -18913,6 +18910,9 @@ }, "Secondary" : { + }, + "Secondary Admin Key" : { + }, "Security" : { @@ -21031,6 +21031,9 @@ }, "Ten Minutes" : { + }, + "Tertiary Admin Key" : { + }, "The amount of time to wait before we consider your packet as done." : { @@ -21062,7 +21065,7 @@ "The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again, but this also may indicate a more serious security problem. Contact the user through another trusted channel to determine if the key change was due to a factory reset or other intentional action." : { }, - "The public key authorized to send admin messages to this node." : { + "The primary public key authorized to send admin messages to this node." : { }, "The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action." : { @@ -21073,9 +21076,15 @@ }, "The root topic to use for MQTT." : { + }, + "The secondary public key authorized to send admin messages to this node." : { + }, "The state of the LED (on/off)" : { + }, + "The tertiarypublic key authorized to send admin messages to this node." : { + }, "There has been no response to a request for device metadata over the admin channel for this node." : { @@ -22607,9 +22616,6 @@ }, "When using in GPIO mode, keep the output on for this long. " : { - }, - "Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)" : { - }, "WiFi Options" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c6706b44..e870495b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -299,6 +299,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 = ""; }; + DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = ""; }; DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = ""; }; DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; @@ -1910,6 +1911,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */, DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */, DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, @@ -1956,7 +1958,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */; + currentVersion = DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index b0dd9966..2a2d7090 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -25,9 +25,12 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case th = 12 case ua433 = 14 case ua868 = 15 - case my_433 = 16 - case my_919 = 17 - case sg_923 = 18 + case my433 = 16 + case my919 = 17 + case sg923 = 18 + case ph433 = 19 + case ph868 = 20 + case ph915 = 21 case lora24 = 13 var topic: String { switch self { @@ -61,12 +64,18 @@ enum RegionCodes: Int, CaseIterable, Identifiable { "UA_433" case .ua868: "UA_868" - case .my_433: + case .my433: "MY_433" - case .my_919: + case .my919: "MY_919" - case .sg_923: + case .sg923: "SG_923" + case .ph433: + "ph_433" + case .ph868: + "ph_868" + case .ph915: + "ph_915" case .lora24: "LORA_24" } } @@ -105,12 +114,18 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Ukraine 868mhz" case .lora24: return "2.4 GHZ" - case .my_433: + case .my433: return "Malaysia 433mhz" - case .my_919: + case .my919: return "Malaysia 919mhz" - case .sg_923: + case .sg923: return "Singapore 923mhz" + case .ph433: + return "Philippines 433mhz" + case .ph868: + return "Philippines 868mhz" + case .ph915: + return "Philippines 915mhz" } } var dutyCycle: Int { @@ -147,11 +162,17 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return 10 case .lora24: return 100 - case .my_433: + case .my433: return 100 - case .my_919: + case .my919: return 100 - case .sg_923: + case .sg923: + return 100 + case .ph433: + return 100 + case .ph868: + return 100 + case .ph915: return 100 } } @@ -190,12 +211,18 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return Config.LoRaConfig.RegionCode.ua868 case .lora24: return Config.LoRaConfig.RegionCode.lora24 - case .my_433: + case .my433: return Config.LoRaConfig.RegionCode.my433 - case .my_919: + case .my919: return Config.LoRaConfig.RegionCode.my919 - case .sg_923: + case .sg923: return Config.LoRaConfig.RegionCode.sg923 + case .ph433: + return Config.LoRaConfig.RegionCode.ph433 + case .ph868: + return Config.LoRaConfig.RegionCode.ph868 + case .ph915: + return Config.LoRaConfig.RegionCode.ph915 } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 9c4ed69c..803c4dff 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -780,6 +780,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage sentPackets: UInt32(telemetry.numPacketsTx), receivedPackets: UInt32(telemetry.numPacketsRx), badReceivedPackets: UInt32(telemetry.numPacketsRxBad), + dupeReceivedPackets: UInt32(telemetry.numRxDupe), + packetsSentRelay: UInt32(telemetry.numTxRelay), + packetsCanceledRelay: UInt32(telemetry.numTxRelayCanceled), nodesOnline: UInt32(telemetry.numOnlineNodes), totalNodes: UInt32(telemetry.numTotalNodes), timerRange: date) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 4269da21..6d376a5f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 45.xcdatamodel + MeshtasticDataModelV 46.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents new file mode 100644 index 00000000..a79a311f --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 69757588..548ead43 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -989,7 +989,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso newConfig.sendBell = config.sendBell newConfig.name = config.name newConfig.monitorPin = Int32(config.monitorPin) - newConfig.detectionTriggeredHigh = config.detectionTriggeredHigh + newConfig.detectionTriggeredHigh = config.detectionTriggerType == .logicHigh ? true : false newConfig.usePullup = config.usePullup newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) @@ -1000,7 +1000,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso fetchedNode[0].detectionSensorConfig?.name = config.name fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin) fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup - fetchedNode[0].detectionSensorConfig?.detectionTriggeredHigh = config.detectionTriggeredHigh + fetchedNode[0].detectionSensorConfig?.detectionTriggeredHigh = config.detectionTriggerType == .logicHigh ? true : false fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index c26b7676..be466530 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -341,6 +341,9 @@ struct Connect: View { sentPackets: UInt32(mostRecent?.numPacketsTx ?? 0), receivedPackets: UInt32(mostRecent?.numPacketsRx ?? 0), badReceivedPackets: UInt32(mostRecent?.numPacketsRxBad ?? 0), + dupeReceivedPackets: UInt32(mostRecent?.numRxDupe ?? 0), + packetsSentRelay: UInt32(mostRecent?.numTxRelay ?? 0), + packetsCanceledRelay: UInt32(mostRecent?.numTxRelayCanceled ?? 0), nodesOnline: UInt32(mostRecent?.numOnlineNodes ?? 0), totalNodes: UInt32(mostRecent?.numTotalNodes ?? 0), timerRange: Date.now...future) diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index a2994747..6bcab1d0 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -12,7 +12,7 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String @Binding private var isValid: Bool - @State private var isSecure: Bool = true + @State var isSecure: Bool = true private var title: String init(_ title: String, text: Binding, isValid: Binding) { diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index eb50a80f..076cb016 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -116,11 +116,11 @@ struct DetectionSensorConfig: View { } .pickerStyle(DefaultPickerStyle()) - Toggle(isOn: $detectionTriggeredHigh) { - Label("Detection trigger High", systemImage: "dial.high") - Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// Toggle(isOn: $detectionTriggeredHigh) { +// Label("Detection trigger High", systemImage: "dial.high") +// Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $usePullup) { Label("Uses pullup resistor", systemImage: "arrow.up.to.line") @@ -166,7 +166,7 @@ struct DetectionSensorConfig: View { dsc.sendBell = self.sendBell dsc.name = self.name dsc.monitorPin = UInt32(self.monitorPin) - dsc.detectionTriggeredHigh = self.detectionTriggeredHigh + // dsc.detectionTriggeredHigh = self.detectionTriggeredHigh dsc.usePullup = self.usePullup dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs) dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs) @@ -217,7 +217,7 @@ struct DetectionSensorConfig: View { if newSendBell != node?.detectionSensorConfig?.sendBell { hasChanges = true } } .onChange(of: detectionTriggeredHigh) { _, newDetectionTriggeredHigh in - if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true } + // if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true } } .onChange(of: usePullup) { _, newUsePullup in if newUsePullup != node?.detectionSensorConfig?.usePullup { hasChanges = true } @@ -244,7 +244,7 @@ struct DetectionSensorConfig: View { self.name = (node?.detectionSensorConfig?.name ?? "") self.monitorPin = Int(node?.detectionSensorConfig?.monitorPin ?? 0) self.usePullup = (node?.detectionSensorConfig?.usePullup ?? false) - self.detectionTriggeredHigh = (node?.detectionSensorConfig?.detectionTriggeredHigh ?? true) + // self.detectionTriggeredHigh = (node?.detectionSensorConfig?.detectionTriggeredHigh ?? true) self.minimumBroadcastSecs = Int(node?.detectionSensorConfig?.minimumBroadcastSecs ?? 45) self.stateBroadcastSecs = Int(node?.detectionSensorConfig?.stateBroadcastSecs ?? 0) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 75b69463..8ddd5816 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -26,7 +26,11 @@ struct SecurityConfig: View { @State var privateKey = "" @State var hasValidPrivateKey: Bool = false @State var adminKey = "" + @State var adminKey2 = "" + @State var adminKey3 = "" @State var hasValidAdminKey: Bool = true + @State var hasValidAdminKey2: Bool = true + @State var hasValidAdminKey3: Bool = true @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false @@ -49,8 +53,7 @@ struct SecurityConfig: View { Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) - } - VStack(alignment: .leading) { + Divider() Label("Private Key", systemImage: "key.fill") SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) .background( @@ -60,11 +63,34 @@ struct SecurityConfig: View { Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) - } - VStack(alignment: .leading) { - Label("Admin Key", systemImage: "key.viewfinder") - SecureInput("Admin Key", text: $adminKey, isValid: $hasValidAdminKey) - Text("The public key authorized to send admin messages to this node.") + Divider() + Label("Primary Admin Key", systemImage: "key.viewfinder") + SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The primary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Secondary Admin Key", systemImage: "key.viewfinder") + SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The secondary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Tertiary Admin Key", systemImage: "key.viewfinder") + SecureInput("Tertiary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The tertiarypublic key authorized to send admin messages to this node.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } @@ -147,6 +173,28 @@ struct SecurityConfig: View { } hasChanges = true } + .onChange(of: adminKey2) { _, key in + let tempKey = Data(base64Encoded: key) ?? Data() + if key.isEmpty { + hasValidAdminKey2 = true + } else if tempKey.count == 32 { + hasValidAdminKey2 = true + } else { + hasValidAdminKey2 = false + } + hasChanges = true + } + .onChange(of: adminKey3) { _, key in + let tempKey = Data(base64Encoded: key) ?? Data() + if key.isEmpty { + hasValidAdminKey3 = true + } else if tempKey.count == 32 { + hasValidAdminKey3 = true + } else { + hasValidAdminKey3 = false + } + hasChanges = true + } .onFirstAppear { // Need to request a DeviceConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { @@ -186,7 +234,7 @@ struct SecurityConfig: View { var config = Config.SecurityConfig() config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() - config.adminKey = [Data(base64Encoded: adminKey) ?? Data()] + config.adminKey = [Data(base64Encoded: adminKey) ?? Data(), Data(base64Encoded: adminKey2) ?? Data(), Data(base64Encoded: adminKey3) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled @@ -211,6 +259,8 @@ struct SecurityConfig: View { self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? "" self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" + self.adminKey2 = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" + self.adminKey3 = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false diff --git a/Widgets/MeshActivityAttributes.swift b/Widgets/MeshActivityAttributes.swift index a2abdbba..876b75de 100644 --- a/Widgets/MeshActivityAttributes.swift +++ b/Widgets/MeshActivityAttributes.swift @@ -21,8 +21,13 @@ struct MeshActivityAttributes: ActivityAttributes { var sentPackets: UInt32 var receivedPackets: UInt32 var badReceivedPackets: UInt32 + var dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 var nodesOnline: UInt32 var totalNodes: UInt32 + + public var numTxRelayCanceled: UInt32 = 0 var timerRange: ClosedRange } diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index e16e3913..efc6ca11 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -122,7 +122,7 @@ struct WidgetsLiveActivity: Widget { struct WidgetsLiveActivity_Previews: PreviewProvider { static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") - static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) + static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, dupeReceivedPackets: 100 , packetsSentRelay: 250, packetsCanceledRelay: 372, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) static var previews: some View { attributes From 4ce03d2a49a8be57c201673b09e1bebdf38ebd64 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 10 Oct 2024 07:05:03 -0700 Subject: [PATCH 263/333] Comment out mapkit views --- Localizable.xcstrings | 3 - .../Views/MapKitMap/Custom/MapButtons.swift | 118 +-- .../MapKitMap/Custom/MapViewSwiftUI.swift | 864 +++++++++--------- .../Views/MapKitMap/WaypointFormMapKit.swift | 526 +++++------ Settings.bundle/Root.plist | 10 - 5 files changed, 754 insertions(+), 767 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index da00af42..3a3c9a4b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -10878,9 +10878,6 @@ }, "Location:" : { - }, - "Location: %@" : { - }, "Locked" : { diff --git a/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift b/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift index 2d45b4f4..89959f74 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift @@ -1,64 +1,64 @@ // -// MapButtons.swift -// Meshtastic +//// MapButtons.swift +//// Meshtastic +//// +//// Copyright © Garth Vander Houwen 4/23/23. +//// // -// Copyright © Garth Vander Houwen 4/23/23. +//import SwiftUI // - -import SwiftUI - -struct MapButtons: View { - let buttonWidth: CGFloat = 22 - let width: CGFloat = 45 - @Binding var tracking: UserTrackingModes - @Binding var isPresentingInfoSheet: Bool - var body: some View { - VStack { - let impactLight = UIImpactFeedbackGenerator(style: .light) - Button(action: { - self.isPresentingInfoSheet.toggle() - }) { - Image(systemName: isPresentingInfoSheet ? "info.circle.fill" : "info.circle") - .resizable() - .frame(width: buttonWidth, height: buttonWidth, alignment: .center) - .offset(y: -2) - } - Divider() - Button(action: { - switch self.tracking { - case .none: - self.tracking = .follow - case .follow: - self.tracking = .followWithHeading - case .followWithHeading: - self.tracking = .none - } - impactLight.impactOccurred() - }) { - Image(systemName: tracking.icon) - .frame(width: buttonWidth, height: buttonWidth, alignment: .center) - .offset(y: 3) - } - } - .frame(width: width, height: width*2, alignment: .center) - .background(Color(UIColor.systemBackground)) - .cornerRadius(8) - .shadow(radius: 1) - .offset(x: 3, y: 25) - } -} - -// MARK: Previews -// struct MapControl_Previews: PreviewProvider { -// @State static var tracking: UserTrackingModes = .none -// @State static var isPresentingInfoSheet = false -// static var previews: some View { -// Group { -// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) -// .environment(\.colorScheme, .light) -// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) -// .environment(\.colorScheme, .dark) +//struct MapButtons: View { +// let buttonWidth: CGFloat = 22 +// let width: CGFloat = 45 +// @Binding var tracking: UserTrackingModes +// @Binding var isPresentingInfoSheet: Bool +// var body: some View { +// VStack { +// let impactLight = UIImpactFeedbackGenerator(style: .light) +// Button(action: { +// self.isPresentingInfoSheet.toggle() +// }) { +// Image(systemName: isPresentingInfoSheet ? "info.circle.fill" : "info.circle") +// .resizable() +// .frame(width: buttonWidth, height: buttonWidth, alignment: .center) +// .offset(y: -2) +// } +// Divider() +// Button(action: { +// switch self.tracking { +// case .none: +// self.tracking = .follow +// case .follow: +// self.tracking = .followWithHeading +// case .followWithHeading: +// self.tracking = .none +// } +// impactLight.impactOccurred() +// }) { +// Image(systemName: tracking.icon) +// .frame(width: buttonWidth, height: buttonWidth, alignment: .center) +// .offset(y: 3) +// } // } -// .previewLayout(.fixed(width: 60, height: 100)) +// .frame(width: width, height: width*2, alignment: .center) +// .background(Color(UIColor.systemBackground)) +// .cornerRadius(8) +// .shadow(radius: 1) +// .offset(x: 3, y: 25) // } -// } +//} +// +//// MARK: Previews +//// struct MapControl_Previews: PreviewProvider { +//// @State static var tracking: UserTrackingModes = .none +//// @State static var isPresentingInfoSheet = false +//// static var previews: some View { +//// Group { +//// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) +//// .environment(\.colorScheme, .light) +//// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) +//// .environment(\.colorScheme, .dark) +//// } +//// .previewLayout(.fixed(width: 60, height: 100)) +//// } +//// } diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index b7629326..8cdf6d84 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -1,434 +1,434 @@ +//// +//// MapViewSwitUI.swift +//// Meshtastic +//// +//// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. // -// MapViewSwitUI.swift -// Meshtastic +//import Foundation +//import SwiftUI +//import MapKit +//import OSLog // -// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. - -import Foundation -import SwiftUI -import MapKit -import OSLog - -struct PolygonInfo: Codable { - let stroke: String? - let strokeWidth, strokeOpacity: Int? - let fill: String? - let fillOpacity: Double? - let title, subtitle: String? -} - -func degreesToRadians(_ number: Double) -> Double { - return number * .pi / 180 -} -var currentMapLayer: MapLayer? - -struct MapViewSwiftUI: UIViewRepresentable { - var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void - var onWaypointEdit: (_ waypointId: Int ) -> Void - let mapView = MKMapView() - // Parameters - let selectedMapLayer: MapLayer - let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer - let positions: [PositionEntity] - let waypoints: [WaypointEntity] - let userTrackingMode: MKUserTrackingMode - let showNodeHistory: Bool - let showRouteLines: Bool - let mapViewType: MKMapType = MKMapType.standard - // Offline Map Tiles - @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 - @State private var loadedLastUpdatedLocalMapFile = 0 - var customMapOverlay: CustomMapOverlay? - @State private var presentCustomMapOverlayHash: CustomMapOverlay? - // MARK: Private methods - private func configureMap(mapView: MKMapView) { - // Map View Parameters - mapView.mapType = mapViewType - mapView.addAnnotations(waypoints) - // Do the initial map centering - let latest = positions - .filter { $0.latest == true } - .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) - let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation - let region = MKCoordinateRegion(center: center, span: span) - mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.setRegion(region, animated: true) - // Set user (phone gps) tracking options - mapView.setUserTrackingMode(userTrackingMode, animated: true) - if userTrackingMode == MKUserTrackingMode.none { - if latest.count == 1 { - mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false) - } else { - mapView.fitAllAnnotations() - } - mapView.showsUserLocation = false - } else { - mapView.showsUserLocation = true - } - // Other MKMapView Settings - mapView.preferredConfiguration.elevationStyle = .realistic// .flat - mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll - mapView.isPitchEnabled = true - mapView.isRotateEnabled = true - mapView.isScrollEnabled = true - mapView.isZoomEnabled = true - mapView.showsBuildings = true - mapView.showsScale = true - mapView.showsTraffic = true - - mapView.showsCompass = false - let compass = MKCompassButton(mapView: mapView) - compass.translatesAutoresizingMaskIntoConstraints = false - #if targetEnvironment(macCatalyst) - // Show the default always visible compass and the mac only controls - compass.compassVisibility = .visible - mapView.addSubview(compass) - mapView.showsZoomControls = true - mapView.showsPitchControl = true - compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -115).isActive = true - compass.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -5).isActive = true - #else - compass.compassVisibility = .adaptive - mapView.addSubview(compass) - compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true - compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true - #endif - } - private func setMapBaseLayer(mapView: MKMapView) { - // Avoid refreshing UI if selectedLayer has not changed - guard currentMapLayer != selectedMapLayer else { return } - currentMapLayer = selectedMapLayer - for overlay in mapView.overlays where overlay is MKTileOverlay { - mapView.removeOverlay(overlay) - } - switch selectedMapLayer { - case .offline: - mapView.mapType = .standard - let overlay = TileOverlay() - overlay.canReplaceMapContent = false - overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex - overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex - mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) - case .satellite: - mapView.mapType = .satellite - case .hybrid: - mapView.mapType = .hybrid - default: - mapView.mapType = .standard - } - } - private func setMapOverlays(mapView: MKMapView) { - // Weather radar - if UserDefaults.enableOverlayServer { - let locale = Locale.current - if locale.region?.identifier ?? "no locale" == "US" { - let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl) - overlay.canReplaceMapContent = false - overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex - overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex - mapView.addOverlay(overlay, level: .aboveLabels) - } - } - } - - func makeUIView(context: Context) -> MKMapView { - currentMapLayer = nil - mapView.delegate = context.coordinator - self.configureMap(mapView: mapView) - return mapView - } - func updateUIView(_ mapView: MKMapView, context: Context) { - // Set selected map base layer - setMapBaseLayer(mapView: mapView) - // Set map tile server and weather overlay layers - setMapOverlays(mapView: mapView) - let latest = positions - .filter { $0.latest == true } - .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - // Node Route Lines - if showRouteLines { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays where overlay is MKPolyline { - mapView.removeOverlay(overlay) - } - var lineIndex = 0 - for position in latest { - let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } - let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationHelper.DefaultLocation - }) - let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))" - mapView.addOverlay(polyline, level: .aboveLabels) - lineIndex += 1 - // There are 18 colors for lines, start over if we are at index 17 - if lineIndex > 17 { - lineIndex = 0 - } - } - } else { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays where overlay is MKPolyline { - mapView.removeOverlay(overlay) - } - } - let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) - if annotationCount != mapView.annotations.count { - Logger.services.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") - mapView.removeAnnotations(mapView.annotations) - mapView.addAnnotations(waypoints) - } - mapView.addAnnotations(showNodeHistory ? positions : latest) - if userTrackingMode == MKUserTrackingMode.none { - mapView.showsUserLocation = false - if UserDefaults.enableMapRecentering { - if latest.count == 1 { - mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true) - } else { - mapView.fitAllAnnotations() - } - } - } else { - mapView.showsUserLocation = true - } - mapView.setUserTrackingMode(userTrackingMode, animated: true) - } - func makeCoordinator() -> MapCoordinator { - return Coordinator(self) - } - final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { - var parent: MapViewSwiftUI - var longPressRecognizer = UILongPressGestureRecognizer() - init(_ parent: MapViewSwiftUI) { - self.parent = parent - super.init() - self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) - self.longPressRecognizer.minimumPressDuration = 0.5 - self.longPressRecognizer.cancelsTouchesInView = true - self.longPressRecognizer.delegate = self - self.parent.mapView.addGestureRecognizer(longPressRecognizer) - } - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - switch annotation { - case let positionAnnotation as PositionEntity: - let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) - annotationView.tag = -1 - annotationView.canShowCallout = true - if positionAnnotation.latest { - annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).darker() - annotationView.displayPriority = .required - annotationView.titleVisibility = .visible - } else { - annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).lighter() - annotationView.displayPriority = .defaultHigh - annotationView.titleVisibility = .adaptive - } - annotationView.tag = -1 - annotationView.canShowCallout = true - annotationView.titleVisibility = .adaptive - let leftIcon = UIImageView(image: annotationView.glyphText?.image()) - leftIcon.backgroundColor = UIColor(.indigo) - annotationView.leftCalloutAccessoryView = leftIcon - let subtitle = UILabel() - subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n" - subtitle.text? += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n" - subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n" - let distanceFormatter = MKDistanceFormatter() - subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" - if positionAnnotation.nodePosition?.metadata != nil { - if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || - DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || - DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient { - annotationView.glyphImage = UIImage(systemName: "flipphone") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater { - annotationView.glyphImage = UIImage(systemName: "repeat") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router { - annotationView.glyphImage = UIImage(systemName: "wifi.router.fill") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker { - annotationView.glyphImage = UIImage(systemName: "location.viewfinder") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { - annotationView.glyphImage = UIImage(systemName: "sensor") - } - let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) - if pf.contains(.Satsinview) { - subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" - } - if pf.contains(.SeqNo) { - subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" - } - if pf.contains(.Heading) { - if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { - annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) - subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" - } else { - annotationView.glyphImage = UIImage(systemName: "flipphone") - } - } - if pf.contains(.Speed) { - let formatter = MeasurementFormatter() - formatter.locale = Locale.current - if positionAnnotation.speed <= 1 { - annotationView.glyphImage = UIImage(systemName: "hexagon") - } - subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" - } - } else { - // node metadata is nil - annotationView.glyphImage = UIImage(systemName: "flipphone") - } - if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { - let metersAway = positionAnnotation.coordinate.distance(from: LocationHelper.currentLocation) - subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" - } - subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n" - subtitle.numberOfLines = 0 - annotationView.detailCalloutAccessoryView = subtitle - let detailsIcon = UIButton(type: .detailDisclosure) - detailsIcon.setImage(UIImage(systemName: "trash"), for: .normal) - annotationView.rightCalloutAccessoryView = detailsIcon - return annotationView - case let waypointAnnotation as WaypointEntity: - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id)) - annotationView.tag = Int(waypointAnnotation.id) - annotationView.isEnabled = true - annotationView.canShowCallout = true - if waypointAnnotation.icon == 0 { - annotationView.glyphText = "📍" - } else { - annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") - } - annotationView.markerTintColor = UIColor(.accentColor) - annotationView.displayPriority = .required - annotationView.titleVisibility = .adaptive - let leftIcon = UIImageView(image: annotationView.glyphText?.image()) - leftIcon.backgroundColor = UIColor(.accentColor) - annotationView.leftCalloutAccessoryView = leftIcon - let subtitle = UILabel() - if waypointAnnotation.longDescription?.count ?? 0 > 0 { - subtitle.text = (waypointAnnotation.longDescription ?? "") + "\n" - } else { - subtitle.text = "" - } - if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { - let metersAway = waypointAnnotation.coordinate.distance(from: LocationHelper.currentLocation) - let distanceFormatter = MKDistanceFormatter() - subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" - } - if waypointAnnotation.created != nil { - subtitle.text! += "Created: \(waypointAnnotation.created?.formatted() ?? "Unknown") \n" - } - if waypointAnnotation.lastUpdated != nil { - subtitle.text! += "Updated: \(waypointAnnotation.lastUpdated?.formatted() ?? "Unknown") \n" - } - if waypointAnnotation.expire != nil { - subtitle.text! += "Expires: \(waypointAnnotation.expire?.formatted() ?? "Unknown") \n" - } - subtitle.numberOfLines = 0 - annotationView.detailCalloutAccessoryView = subtitle - let editIcon = UIButton(type: .detailDisclosure) - editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) - annotationView.rightCalloutAccessoryView = editIcon - return annotationView - default: return nil - } - } - func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { - switch view.annotation { - case _ as WaypointEntity: - // Only Allow Edit for waypoint annotations with a id - if view.tag > 0 { - parent.onWaypointEdit(view.tag) - } - default: break - } - } - @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - if gesture.state != UIGestureRecognizer.State.ended { - return - } else if gesture.state != UIGestureRecognizer.State.began { - // Screen Position - CGPoint - let location = longPressRecognizer.location(in: self.parent.mapView) - // Map Coordinate - CLLocationCoordinate2D - let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - let annotation = MKPointAnnotation() - annotation.title = "Dropped Pin" - annotation.coordinate = coordinate - parent.mapView.addAnnotation(annotation) - UINotificationFeedbackGenerator().notificationOccurred(.success) - parent.onLongPress(coordinate) - } - } - public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - if let tileOverlay = overlay as? MKTileOverlay { - return MKTileOverlayRenderer(tileOverlay: tileOverlay) - } else { - if let routePolyline = overlay as? MKPolyline { - let titleString = routePolyline.title ?? "0" - let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0).lighter() - renderer.lineWidth = 8 - return renderer - } - if let polygon = overlay as? MKPolygon { - let renderer = MKPolygonRenderer(polygon: polygon) - renderer.fillColor = UIColor.purple.withAlphaComponent(0.2) - renderer.strokeColor = .purple.withAlphaComponent(0.7) - return renderer - } - return MKOverlayRenderer(overlay: overlay) - } - } - } - /// is supposed to be located in the folder with the map name - public struct DefaultTile: Hashable { - let tileName: String - let tileType: String - public init(tileName: String, tileType: String) { - self.tileName = tileName - self.tileType = tileType - } - } - public struct CustomMapOverlay: Equatable, Hashable { - let mapName: String - let tileType: String - var canReplaceMapContent: Bool - var minimumZoomLevel: Int? - var maximumZoomLevel: Int? - let defaultTile: DefaultTile? - public init( - mapName: String, - tileType: String, - canReplaceMapContent: Bool = true, // false for transparent tiles - minimumZoomLevel: Int? = nil, - maximumZoomLevel: Int? = nil, - defaultTile: DefaultTile? = nil - ) { - self.mapName = mapName - self.tileType = tileType - self.canReplaceMapContent = canReplaceMapContent - self.minimumZoomLevel = minimumZoomLevel - self.maximumZoomLevel = maximumZoomLevel - self.defaultTile = defaultTile - } - public init?( - mapName: String?, - tileType: String, - canReplaceMapContent: Bool = true, // false for transparent tiles - minimumZoomLevel: Int? = nil, - maximumZoomLevel: Int? = nil, - defaultTile: DefaultTile? = nil - ) { - if mapName == nil || mapName! == "" { - return nil - } - self.mapName = mapName! - self.tileType = tileType - self.canReplaceMapContent = canReplaceMapContent - self.minimumZoomLevel = minimumZoomLevel - self.maximumZoomLevel = maximumZoomLevel - self.defaultTile = defaultTile - } - } -} +//struct PolygonInfo: Codable { +// let stroke: String? +// let strokeWidth, strokeOpacity: Int? +// let fill: String? +// let fillOpacity: Double? +// let title, subtitle: String? +//} +// +//func degreesToRadians(_ number: Double) -> Double { +// return number * .pi / 180 +//} +//var currentMapLayer: MapLayer? +// +//struct MapViewSwiftUI: UIViewRepresentable { +// var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void +// var onWaypointEdit: (_ waypointId: Int ) -> Void +// let mapView = MKMapView() +// // Parameters +// let selectedMapLayer: MapLayer +// let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer +// let positions: [PositionEntity] +// let waypoints: [WaypointEntity] +// let userTrackingMode: MKUserTrackingMode +// let showNodeHistory: Bool +// let showRouteLines: Bool +// let mapViewType: MKMapType = MKMapType.standard +// // Offline Map Tiles +// @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 +// @State private var loadedLastUpdatedLocalMapFile = 0 +// var customMapOverlay: CustomMapOverlay? +// @State private var presentCustomMapOverlayHash: CustomMapOverlay? +// // MARK: Private methods +// private func configureMap(mapView: MKMapView) { +// // Map View Parameters +// mapView.mapType = mapViewType +// mapView.addAnnotations(waypoints) +// // Do the initial map centering +// let latest = positions +// .filter { $0.latest == true } +// .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } +// let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) +// let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation +// let region = MKCoordinateRegion(center: center, span: span) +// mapView.addAnnotations(showNodeHistory ? positions : latest) +// mapView.setRegion(region, animated: true) +// // Set user (phone gps) tracking options +// mapView.setUserTrackingMode(userTrackingMode, animated: true) +// if userTrackingMode == MKUserTrackingMode.none { +// if latest.count == 1 { +// mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false) +// } else { +// mapView.fitAllAnnotations() +// } +// mapView.showsUserLocation = false +// } else { +// mapView.showsUserLocation = true +// } +// // Other MKMapView Settings +// mapView.preferredConfiguration.elevationStyle = .realistic// .flat +// mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll +// mapView.isPitchEnabled = true +// mapView.isRotateEnabled = true +// mapView.isScrollEnabled = true +// mapView.isZoomEnabled = true +// mapView.showsBuildings = true +// mapView.showsScale = true +// mapView.showsTraffic = true +// +// mapView.showsCompass = false +// let compass = MKCompassButton(mapView: mapView) +// compass.translatesAutoresizingMaskIntoConstraints = false +// #if targetEnvironment(macCatalyst) +// // Show the default always visible compass and the mac only controls +// compass.compassVisibility = .visible +// mapView.addSubview(compass) +// mapView.showsZoomControls = true +// mapView.showsPitchControl = true +// compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -115).isActive = true +// compass.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -5).isActive = true +// #else +// compass.compassVisibility = .adaptive +// mapView.addSubview(compass) +// compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true +// compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true +// #endif +// } +// private func setMapBaseLayer(mapView: MKMapView) { +// // Avoid refreshing UI if selectedLayer has not changed +// guard currentMapLayer != selectedMapLayer else { return } +// currentMapLayer = selectedMapLayer +// for overlay in mapView.overlays where overlay is MKTileOverlay { +// mapView.removeOverlay(overlay) +// } +// switch selectedMapLayer { +// case .offline: +// mapView.mapType = .standard +// let overlay = TileOverlay() +// overlay.canReplaceMapContent = false +// overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex +// overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex +// mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) +// case .satellite: +// mapView.mapType = .satellite +// case .hybrid: +// mapView.mapType = .hybrid +// default: +// mapView.mapType = .standard +// } +// } +// private func setMapOverlays(mapView: MKMapView) { +// // Weather radar +// if UserDefaults.enableOverlayServer { +// let locale = Locale.current +// if locale.region?.identifier ?? "no locale" == "US" { +// let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl) +// overlay.canReplaceMapContent = false +// overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex +// overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex +// mapView.addOverlay(overlay, level: .aboveLabels) +// } +// } +// } +// +// func makeUIView(context: Context) -> MKMapView { +// currentMapLayer = nil +// mapView.delegate = context.coordinator +// self.configureMap(mapView: mapView) +// return mapView +// } +// func updateUIView(_ mapView: MKMapView, context: Context) { +// // Set selected map base layer +// setMapBaseLayer(mapView: mapView) +// // Set map tile server and weather overlay layers +// setMapOverlays(mapView: mapView) +// let latest = positions +// .filter { $0.latest == true } +// .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } +// // Node Route Lines +// if showRouteLines { +// // Remove all existing PolyLine Overlays +// for overlay in mapView.overlays where overlay is MKPolyline { +// mapView.removeOverlay(overlay) +// } +// var lineIndex = 0 +// for position in latest { +// let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } +// let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in +// return position.nodeCoordinate ?? LocationHelper.DefaultLocation +// }) +// let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) +// polyline.title = "\(String(position.nodePosition?.num ?? 0))" +// mapView.addOverlay(polyline, level: .aboveLabels) +// lineIndex += 1 +// // There are 18 colors for lines, start over if we are at index 17 +// if lineIndex > 17 { +// lineIndex = 0 +// } +// } +// } else { +// // Remove all existing PolyLine Overlays +// for overlay in mapView.overlays where overlay is MKPolyline { +// mapView.removeOverlay(overlay) +// } +// } +// let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) +// if annotationCount != mapView.annotations.count { +// Logger.services.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") +// mapView.removeAnnotations(mapView.annotations) +// mapView.addAnnotations(waypoints) +// } +// mapView.addAnnotations(showNodeHistory ? positions : latest) +// if userTrackingMode == MKUserTrackingMode.none { +// mapView.showsUserLocation = false +// if UserDefaults.enableMapRecentering { +// if latest.count == 1 { +// mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true) +// } else { +// mapView.fitAllAnnotations() +// } +// } +// } else { +// mapView.showsUserLocation = true +// } +// mapView.setUserTrackingMode(userTrackingMode, animated: true) +// } +// func makeCoordinator() -> MapCoordinator { +// return Coordinator(self) +// } +// final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { +// var parent: MapViewSwiftUI +// var longPressRecognizer = UILongPressGestureRecognizer() +// init(_ parent: MapViewSwiftUI) { +// self.parent = parent +// super.init() +// self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) +// self.longPressRecognizer.minimumPressDuration = 0.5 +// self.longPressRecognizer.cancelsTouchesInView = true +// self.longPressRecognizer.delegate = self +// self.parent.mapView.addGestureRecognizer(longPressRecognizer) +// } +// func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { +// switch annotation { +// case let positionAnnotation as PositionEntity: +// let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) +// let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) +// annotationView.tag = -1 +// annotationView.canShowCallout = true +// if positionAnnotation.latest { +// annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).darker() +// annotationView.displayPriority = .required +// annotationView.titleVisibility = .visible +// } else { +// annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).lighter() +// annotationView.displayPriority = .defaultHigh +// annotationView.titleVisibility = .adaptive +// } +// annotationView.tag = -1 +// annotationView.canShowCallout = true +// annotationView.titleVisibility = .adaptive +// let leftIcon = UIImageView(image: annotationView.glyphText?.image()) +// leftIcon.backgroundColor = UIColor(.indigo) +// annotationView.leftCalloutAccessoryView = leftIcon +// let subtitle = UILabel() +// subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n" +// subtitle.text? += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n" +// subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n" +// let distanceFormatter = MKDistanceFormatter() +// subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" +// if positionAnnotation.nodePosition?.metadata != nil { +// if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || +// DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || +// DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient { +// annotationView.glyphImage = UIImage(systemName: "flipphone") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater { +// annotationView.glyphImage = UIImage(systemName: "repeat") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router { +// annotationView.glyphImage = UIImage(systemName: "wifi.router.fill") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker { +// annotationView.glyphImage = UIImage(systemName: "location.viewfinder") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { +// annotationView.glyphImage = UIImage(systemName: "sensor") +// } +// let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) +// if pf.contains(.Satsinview) { +// subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" +// } +// if pf.contains(.SeqNo) { +// subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" +// } +// if pf.contains(.Heading) { +// if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { +// annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) +// subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" +// } else { +// annotationView.glyphImage = UIImage(systemName: "flipphone") +// } +// } +// if pf.contains(.Speed) { +// let formatter = MeasurementFormatter() +// formatter.locale = Locale.current +// if positionAnnotation.speed <= 1 { +// annotationView.glyphImage = UIImage(systemName: "hexagon") +// } +// subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" +// } +// } else { +// // node metadata is nil +// annotationView.glyphImage = UIImage(systemName: "flipphone") +// } +// if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { +// let metersAway = positionAnnotation.coordinate.distance(from: LocationHelper.currentLocation) +// subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" +// } +// subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n" +// subtitle.numberOfLines = 0 +// annotationView.detailCalloutAccessoryView = subtitle +// let detailsIcon = UIButton(type: .detailDisclosure) +// detailsIcon.setImage(UIImage(systemName: "trash"), for: .normal) +// annotationView.rightCalloutAccessoryView = detailsIcon +// return annotationView +// case let waypointAnnotation as WaypointEntity: +// let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id)) +// annotationView.tag = Int(waypointAnnotation.id) +// annotationView.isEnabled = true +// annotationView.canShowCallout = true +// if waypointAnnotation.icon == 0 { +// annotationView.glyphText = "📍" +// } else { +// annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") +// } +// annotationView.markerTintColor = UIColor(.accentColor) +// annotationView.displayPriority = .required +// annotationView.titleVisibility = .adaptive +// let leftIcon = UIImageView(image: annotationView.glyphText?.image()) +// leftIcon.backgroundColor = UIColor(.accentColor) +// annotationView.leftCalloutAccessoryView = leftIcon +// let subtitle = UILabel() +// if waypointAnnotation.longDescription?.count ?? 0 > 0 { +// subtitle.text = (waypointAnnotation.longDescription ?? "") + "\n" +// } else { +// subtitle.text = "" +// } +// if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { +// let metersAway = waypointAnnotation.coordinate.distance(from: LocationHelper.currentLocation) +// let distanceFormatter = MKDistanceFormatter() +// subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" +// } +// if waypointAnnotation.created != nil { +// subtitle.text! += "Created: \(waypointAnnotation.created?.formatted() ?? "Unknown") \n" +// } +// if waypointAnnotation.lastUpdated != nil { +// subtitle.text! += "Updated: \(waypointAnnotation.lastUpdated?.formatted() ?? "Unknown") \n" +// } +// if waypointAnnotation.expire != nil { +// subtitle.text! += "Expires: \(waypointAnnotation.expire?.formatted() ?? "Unknown") \n" +// } +// subtitle.numberOfLines = 0 +// annotationView.detailCalloutAccessoryView = subtitle +// let editIcon = UIButton(type: .detailDisclosure) +// editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) +// annotationView.rightCalloutAccessoryView = editIcon +// return annotationView +// default: return nil +// } +// } +// func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { +// switch view.annotation { +// case _ as WaypointEntity: +// // Only Allow Edit for waypoint annotations with a id +// if view.tag > 0 { +// parent.onWaypointEdit(view.tag) +// } +// default: break +// } +// } +// @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { +// if gesture.state != UIGestureRecognizer.State.ended { +// return +// } else if gesture.state != UIGestureRecognizer.State.began { +// // Screen Position - CGPoint +// let location = longPressRecognizer.location(in: self.parent.mapView) +// // Map Coordinate - CLLocationCoordinate2D +// let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) +// let annotation = MKPointAnnotation() +// annotation.title = "Dropped Pin" +// annotation.coordinate = coordinate +// parent.mapView.addAnnotation(annotation) +// UINotificationFeedbackGenerator().notificationOccurred(.success) +// parent.onLongPress(coordinate) +// } +// } +// public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { +// if let tileOverlay = overlay as? MKTileOverlay { +// return MKTileOverlayRenderer(tileOverlay: tileOverlay) +// } else { +// if let routePolyline = overlay as? MKPolyline { +// let titleString = routePolyline.title ?? "0" +// let renderer = MKPolylineRenderer(polyline: routePolyline) +// renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0).lighter() +// renderer.lineWidth = 8 +// return renderer +// } +// if let polygon = overlay as? MKPolygon { +// let renderer = MKPolygonRenderer(polygon: polygon) +// renderer.fillColor = UIColor.purple.withAlphaComponent(0.2) +// renderer.strokeColor = .purple.withAlphaComponent(0.7) +// return renderer +// } +// return MKOverlayRenderer(overlay: overlay) +// } +// } +// } +// /// is supposed to be located in the folder with the map name +// public struct DefaultTile: Hashable { +// let tileName: String +// let tileType: String +// public init(tileName: String, tileType: String) { +// self.tileName = tileName +// self.tileType = tileType +// } +// } +// public struct CustomMapOverlay: Equatable, Hashable { +// let mapName: String +// let tileType: String +// var canReplaceMapContent: Bool +// var minimumZoomLevel: Int? +// var maximumZoomLevel: Int? +// let defaultTile: DefaultTile? +// public init( +// mapName: String, +// tileType: String, +// canReplaceMapContent: Bool = true, // false for transparent tiles +// minimumZoomLevel: Int? = nil, +// maximumZoomLevel: Int? = nil, +// defaultTile: DefaultTile? = nil +// ) { +// self.mapName = mapName +// self.tileType = tileType +// self.canReplaceMapContent = canReplaceMapContent +// self.minimumZoomLevel = minimumZoomLevel +// self.maximumZoomLevel = maximumZoomLevel +// self.defaultTile = defaultTile +// } +// public init?( +// mapName: String?, +// tileType: String, +// canReplaceMapContent: Bool = true, // false for transparent tiles +// minimumZoomLevel: Int? = nil, +// maximumZoomLevel: Int? = nil, +// defaultTile: DefaultTile? = nil +// ) { +// if mapName == nil || mapName! == "" { +// return nil +// } +// self.mapName = mapName! +// self.tileType = tileType +// self.canReplaceMapContent = canReplaceMapContent +// self.minimumZoomLevel = minimumZoomLevel +// self.maximumZoomLevel = maximumZoomLevel +// self.defaultTile = defaultTile +// } +// } +//} diff --git a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index ed48e1a4..456472e0 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -1,266 +1,266 @@ +//// +//// WaypointFormView.swift +//// Meshtastic +//// +//// Copyright Garth Vander Houwen 1/10/23. +//// // -// WaypointFormView.swift -// Meshtastic +//import CoreLocation +//import MeshtasticProtobufs +//import OSLog +//import SwiftUI // -// Copyright Garth Vander Houwen 1/10/23. +//struct WaypointFormMapKit: View { // - -import CoreLocation -import MeshtasticProtobufs -import OSLog -import SwiftUI - -struct WaypointFormMapKit: View { - - @EnvironmentObject var bleManager: BLEManager - @Environment(\.dismiss) private var dismiss - @State var coordinate: WaypointCoordinate - @FocusState private var iconIsFocused: Bool - @State private var name: String = "" - @State private var description: String = "" - @State private var icon: String = "📍" - @State private var latitude: Double = 0 - @State private var longitude: Double = 0 - @State private var expires: Bool = false - @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours - @State private var locked: Bool = false - @State private var lockedTo: Int64 = 0 - - var body: some View { - - Form { - let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.coordinate?.latitude ?? 0, longitude: coordinate.coordinate?.longitude ?? 0)) - Section(header: Text((coordinate.waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { - HStack { - Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))") - .textSelection(.enabled) - .foregroundColor(Color.gray) - .font(.caption2) - if coordinate.coordinate?.latitude ?? 0 != 0 && coordinate.coordinate?.longitude ?? 0 != 0 { - DistanceText(meters: distance) - .foregroundColor(Color.gray) - .font(.caption2) - } - } - HStack { - Text("Name") - Spacer() - TextField( - "Name", - text: $name, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: name) { - var totalBytes = name.utf8.count - // Only mess with the value if it is too big - while totalBytes > 30 { - name = String(name.dropLast()) - totalBytes = name.utf8.count - } - if totalBytes > 30 { - name = String(name.dropLast()) - } - } - } - HStack { - Text("Description") - Spacer() - TextField( - "Description", - text: $description, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: description) { - var totalBytes = description.utf8.count - // Only mess with the value if it is too big - while totalBytes > 100 { - description = String(description.dropLast()) - totalBytes = description.utf8.count - } - } - } - HStack { - Text("Icon") - Spacer() - EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") - .font(.title) - .focused($iconIsFocused) - .onChange(of: icon) { _, value in - - // If you have anything other than emojis in your string make it empty - if !value.onlyEmojis() { - icon = "" - } - // If a second emoji is entered delete the first one - if value.count >= 1 { - - if value.count > 1 { - let index = value.index(value.startIndex, offsetBy: 1) - icon = String(value[index]) - } - iconIsFocused = false - } - } - - } - Toggle(isOn: $expires) { - Label("Expires", systemImage: "clock.badge.xmark") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if expires { - DatePicker("Expire", selection: $expire, in: Date.now...) - .datePickerStyle(.compact) - .font(.callout) - } - Toggle(isOn: $locked) { - Label("Locked", systemImage: "lock") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - } - HStack { - Button { - - var newWaypoint = Waypoint() - // Loading a waypoint from edit - if coordinate.waypointId > 0 { - newWaypoint.id = UInt32(coordinate.waypointId) - let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) - newWaypoint.latitudeI = waypoint.latitudeI - newWaypoint.longitudeI = waypoint.longitudeI - } else { - // New waypoint - newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" - newWaypoint.description_p = description - // Unicode scalar value for the icon emoji string - let unicodeScalers = icon.unicodeScalars - // First element as an UInt32 - let unicode = unicodeScalers[unicodeScalers.startIndex].value - newWaypoint.icon = unicode - if locked { - if lockedTo == 0 { - newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) - } else { - newWaypoint.lockedTo = UInt32(lockedTo) - } - } - if expires { - newWaypoint.expire = UInt32(expire.timeIntervalSince1970) - } else { - newWaypoint.expire = 0 - } - if bleManager.sendWaypoint(waypoint: newWaypoint) { - dismiss() - } else { - dismiss() - Logger.mesh.error("Send waypoint failed") - } - } label: { - Label("Send", systemImage: "arrow.up") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .disabled(bleManager.connectedPeripheral == nil) - .padding(.bottom) - - Button(role: .cancel) { - dismiss() - } label: { - Label("cancel", systemImage: "x.circle") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(.bottom) - - if coordinate.waypointId > 0 { - - Menu { - Button("For me", action: { - let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) - bleManager.context.delete(waypoint) - do { - try bleManager.context.save() - } catch { - bleManager.context.rollback() - } - dismiss() }) - Button("For everyone", action: { - var newWaypoint = Waypoint() - - if coordinate.waypointId > 0 { - newWaypoint.id = UInt32(coordinate.waypointId) - } - newWaypoint.name = name.count > 0 ? name : "Dropped Pin" - newWaypoint.description_p = description - newWaypoint.latitudeI = Int32(coordinate.coordinate?.latitude ?? 0 * 1e7) - newWaypoint.longitudeI = Int32(coordinate.coordinate?.longitude ?? 0 * 1e7) - // Unicode scalar value for the icon emoji string - let unicodeScalers = icon.unicodeScalars - // First element as an UInt32 - let unicode = unicodeScalers[unicodeScalers.startIndex].value - newWaypoint.icon = unicode - if locked { - if lockedTo == 0 { - newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) - } else { - newWaypoint.lockedTo = UInt32(lockedTo) - } - } - newWaypoint.expire = 1 - if bleManager.sendWaypoint(waypoint: newWaypoint) { - dismiss() - } else { - dismiss() - Logger.mesh.error("Send waypoint failed") - } - }) - } - label: { - Label("delete", systemImage: "trash") - .foregroundColor(.red) - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(.bottom) - } - } - .onAppear { - if coordinate.waypointId > 0 { - let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) - name = waypoint.name ?? "Dropped Pin" - description = waypoint.longDescription ?? "" - icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") - latitude = Double(waypoint.latitudeI) / 1e7 - longitude = Double(waypoint.longitudeI) / 1e7 - if waypoint.expire != nil { - expires = true - expire = waypoint.expire ?? Date() - } else { - expires = false - } - if waypoint.locked > 0 { - locked = true - lockedTo = waypoint.locked - } - } else { - name = "" - description = "" - locked = false - expires = false - expire = Date.now.addingTimeInterval(60 * 480) - icon = "📍" - latitude = coordinate.coordinate?.latitude ?? 0 - longitude = coordinate.coordinate?.longitude ?? 0 - } - } - } -} +// @EnvironmentObject var bleManager: BLEManager +// @Environment(\.dismiss) private var dismiss +// @State var coordinate: WaypointCoordinate +// @FocusState private var iconIsFocused: Bool +// @State private var name: String = "" +// @State private var description: String = "" +// @State private var icon: String = "📍" +// @State private var latitude: Double = 0 +// @State private var longitude: Double = 0 +// @State private var expires: Bool = false +// @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours +// @State private var locked: Bool = false +// @State private var lockedTo: Int64 = 0 +// +// var body: some View { +// +// Form { +// let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.coordinate?.latitude ?? 0, longitude: coordinate.coordinate?.longitude ?? 0)) +// Section(header: Text((coordinate.waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { +// HStack { +// Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))") +// .textSelection(.enabled) +// .foregroundColor(Color.gray) +// .font(.caption2) +// if coordinate.coordinate?.latitude ?? 0 != 0 && coordinate.coordinate?.longitude ?? 0 != 0 { +// DistanceText(meters: distance) +// .foregroundColor(Color.gray) +// .font(.caption2) +// } +// } +// HStack { +// Text("Name") +// Spacer() +// TextField( +// "Name", +// text: $name, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .onChange(of: name) { +// var totalBytes = name.utf8.count +// // Only mess with the value if it is too big +// while totalBytes > 30 { +// name = String(name.dropLast()) +// totalBytes = name.utf8.count +// } +// if totalBytes > 30 { +// name = String(name.dropLast()) +// } +// } +// } +// HStack { +// Text("Description") +// Spacer() +// TextField( +// "Description", +// text: $description, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .onChange(of: description) { +// var totalBytes = description.utf8.count +// // Only mess with the value if it is too big +// while totalBytes > 100 { +// description = String(description.dropLast()) +// totalBytes = description.utf8.count +// } +// } +// } +// HStack { +// Text("Icon") +// Spacer() +// EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") +// .font(.title) +// .focused($iconIsFocused) +// .onChange(of: icon) { _, value in +// +// // If you have anything other than emojis in your string make it empty +// if !value.onlyEmojis() { +// icon = "" +// } +// // If a second emoji is entered delete the first one +// if value.count >= 1 { +// +// if value.count > 1 { +// let index = value.index(value.startIndex, offsetBy: 1) +// icon = String(value[index]) +// } +// iconIsFocused = false +// } +// } +// +// } +// Toggle(isOn: $expires) { +// Label("Expires", systemImage: "clock.badge.xmark") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// if expires { +// DatePicker("Expire", selection: $expire, in: Date.now...) +// .datePickerStyle(.compact) +// .font(.callout) +// } +// Toggle(isOn: $locked) { +// Label("Locked", systemImage: "lock") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// } +// } +// HStack { +// Button { +// +// var newWaypoint = Waypoint() +// // Loading a waypoint from edit +// if coordinate.waypointId > 0 { +// newWaypoint.id = UInt32(coordinate.waypointId) +// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) +// newWaypoint.latitudeI = waypoint.latitudeI +// newWaypoint.longitudeI = waypoint.longitudeI +// } else { +// // New waypoint +// newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" +// newWaypoint.description_p = description +// // Unicode scalar value for the icon emoji string +// let unicodeScalers = icon.unicodeScalars +// // First element as an UInt32 +// let unicode = unicodeScalers[unicodeScalers.startIndex].value +// newWaypoint.icon = unicode +// if locked { +// if lockedTo == 0 { +// newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) +// } else { +// newWaypoint.lockedTo = UInt32(lockedTo) +// } +// } +// if expires { +// newWaypoint.expire = UInt32(expire.timeIntervalSince1970) +// } else { +// newWaypoint.expire = 0 +// } +// if bleManager.sendWaypoint(waypoint: newWaypoint) { +// dismiss() +// } else { +// dismiss() +// Logger.mesh.error("Send waypoint failed") +// } +// } label: { +// Label("Send", systemImage: "arrow.up") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.regular) +// .disabled(bleManager.connectedPeripheral == nil) +// .padding(.bottom) +// +// Button(role: .cancel) { +// dismiss() +// } label: { +// Label("cancel", systemImage: "x.circle") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.regular) +// .padding(.bottom) +// +// if coordinate.waypointId > 0 { +// +// Menu { +// Button("For me", action: { +// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) +// bleManager.context.delete(waypoint) +// do { +// try bleManager.context.save() +// } catch { +// bleManager.context.rollback() +// } +// dismiss() }) +// Button("For everyone", action: { +// var newWaypoint = Waypoint() +// +// if coordinate.waypointId > 0 { +// newWaypoint.id = UInt32(coordinate.waypointId) +// } +// newWaypoint.name = name.count > 0 ? name : "Dropped Pin" +// newWaypoint.description_p = description +// newWaypoint.latitudeI = Int32(coordinate.coordinate?.latitude ?? 0 * 1e7) +// newWaypoint.longitudeI = Int32(coordinate.coordinate?.longitude ?? 0 * 1e7) +// // Unicode scalar value for the icon emoji string +// let unicodeScalers = icon.unicodeScalars +// // First element as an UInt32 +// let unicode = unicodeScalers[unicodeScalers.startIndex].value +// newWaypoint.icon = unicode +// if locked { +// if lockedTo == 0 { +// newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) +// } else { +// newWaypoint.lockedTo = UInt32(lockedTo) +// } +// } +// newWaypoint.expire = 1 +// if bleManager.sendWaypoint(waypoint: newWaypoint) { +// dismiss() +// } else { +// dismiss() +// Logger.mesh.error("Send waypoint failed") +// } +// }) +// } +// label: { +// Label("delete", systemImage: "trash") +// .foregroundColor(.red) +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.regular) +// .padding(.bottom) +// } +// } +// .onAppear { +// if coordinate.waypointId > 0 { +// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) +// name = waypoint.name ?? "Dropped Pin" +// description = waypoint.longDescription ?? "" +// icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") +// latitude = Double(waypoint.latitudeI) / 1e7 +// longitude = Double(waypoint.longitudeI) / 1e7 +// if waypoint.expire != nil { +// expires = true +// expire = waypoint.expire ?? Date() +// } else { +// expires = false +// } +// if waypoint.locked > 0 { +// locked = true +// lockedTo = waypoint.locked +// } +// } else { +// name = "" +// description = "" +// locked = false +// expires = false +// expire = Date.now.addingTimeInterval(60 * 480) +// icon = "📍" +// latitude = coordinate.coordinate?.latitude ?? 0 +// longitude = coordinate.coordinate?.longitude ?? 0 +// } +// } +// } +//} diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist index 7fb7c0cc..e67ebad7 100644 --- a/Settings.bundle/Root.plist +++ b/Settings.bundle/Root.plist @@ -68,16 +68,6 @@ DefaultValue - - Type - PSToggleSwitchSpecifier - Title - Use Legacy Mesh Map - Key - mapUseLegacy - DefaultValue - - Type PSGroupSpecifier From 8785965197512b53f45affab420f3fb4961e6936 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 10 Oct 2024 20:23:02 -0700 Subject: [PATCH 264/333] Add more nodes to traceroute --- Meshtastic/Helpers/BLEManager.swift | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index e39419af..05e0e68b 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -840,10 +840,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("🪧 \(logString)") } else { + + guard let connectedNode = getNodeInfo(id: Int64(connectedPeripheral.num), context: context) else { + return + } var hopNodes: [TraceRouteHopEntity] = [] let connectedHop = TraceRouteHopEntity(context: context) - connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized connectedHop.time = Date() + connectedHop.num = connectedPeripheral.num + connectedHop.name = connectedNode.user?.longName ?? "???" if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI @@ -851,7 +856,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute?.hasPositions = true } hopNodes.append(connectedHop) - var routeString = "You --> " + var routeString = "\(connectedHop.name ?? "???") --> " for (index, node) in routingMessage.route.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { @@ -879,7 +884,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } - var routeBackString = traceRoute?.node?.user?.longName ?? "unknown".localized + let destinationHop = TraceRouteHopEntity(context: context) + destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized + destinationHop.time = Date() + destinationHop.num = connectedPeripheral.num + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + connectedHop.altitude = mostRecent.altitude + connectedHop.latitudeI = mostRecent.latitudeI + connectedHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + hopNodes.append(destinationHop) + routeString += "\(destinationHop.name ?? "???") (\(destinationHop.num.toHex()))" + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) --> " for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { From b5d7bbd7dc3523e5eaa1f8cb6d7e27cd1b2cfa29 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 10 Oct 2024 22:28:33 -0700 Subject: [PATCH 265/333] Add connected node to end of route back string --- Meshtastic/Helpers/BLEManager.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 05e0e68b..6b90cd89 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -840,7 +840,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("🪧 \(logString)") } else { - guard let connectedNode = getNodeInfo(id: Int64(connectedPeripheral.num), context: context) else { return } @@ -880,8 +879,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } - hopNodes.append(traceRouteHop) } + hopNodes.append(traceRouteHop) routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } let destinationHop = TraceRouteHopEntity(context: context) @@ -921,10 +920,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } - hopNodes.append(traceRouteHop) } + hopNodes.append(traceRouteHop) routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } + routeBackString += "\(connectedHop.name ?? "???") (\(connectedHop.num.toHex()))" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) From 6325eb697c94e33af82e744c3b83dba0d019e485 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Oct 2024 13:51:54 -0700 Subject: [PATCH 266/333] mo hops --- Meshtastic/Helpers/BLEManager.swift | 6 ++++-- .../MeshtasticDataModelV 46.xcdatamodel/contents | 2 ++ Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 11 ++++------- Meshtastic/Views/Nodes/TraceRouteLog.swift | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 6b90cd89..86ecdc96 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -854,8 +854,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } - hopNodes.append(connectedHop) var routeString = "\(connectedHop.name ?? "???") --> " + hopNodes.append(connectedHop) + traceRoute?.hopsTowards = Int32(routingMessage.route.count) for (index, node) in routingMessage.route.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { @@ -896,6 +897,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNodes.append(destinationHop) routeString += "\(destinationHop.name ?? "???") (\(destinationHop.num.toHex()))" var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) --> " + traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { @@ -924,7 +926,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNodes.append(traceRouteHop) routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } - routeBackString += "\(connectedHop.name ?? "???") (\(connectedHop.num.toHex()))" + routeBackString += "\(connectedNode.user?.longName ?? "???") (\(connectedNode.num.toHex()))" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents index a79a311f..8ebdb9e2 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents @@ -420,6 +420,8 @@ + + diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 66f58a07..b75d5822 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -156,39 +156,36 @@ struct NodeListItem: View { Image(systemName: "scroll") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) Text("Logs:") .foregroundColor(.gray) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption2) + .allowsTightening(true) if node.hasDeviceMetrics { Image(systemName: "flipphone") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) } if node.hasPositions { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) + } if node.hasEnvironmentMetrics { Image(systemName: "cloud.sun.rain") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) + } if node.hasDetectionSensorMetrics { Image(systemName: "sensor") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) } if node.hasTraceRoutes { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) } } } diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 398a12b0..3f1a0348 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -37,7 +37,7 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : (route.sent ? "No Response" : "Not Sent"))") + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hopsTowards) \(route.hops?.count ?? 0 == 1 ? "Hop Towards": "Hops Towards")") : (route.sent ? "No Response" : "Not Sent"))") .font(.callout) } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") From b2f6ebd6b307a458142fe5272f33527d39a34cb8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Oct 2024 14:04:49 -0700 Subject: [PATCH 267/333] Smaller font for traceroute list --- Meshtastic/Views/Nodes/TraceRouteLog.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 3f1a0348..bc6a90f0 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -37,8 +37,8 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hopsTowards) \(route.hops?.count ?? 0 == 1 ? "Hop Towards": "Hops Towards")") : (route.sent ? "No Response" : "Not Sent"))") - .font(.callout) + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hopsTowards) \(route.hopsTowards == 1 ? "Hop": "Hops Towards \(route.hopsBack) Hops Back ")") : (route.sent ? "No Response" : "Not Sent"))") + .font(.caption) } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) @@ -62,7 +62,7 @@ struct TraceRouteLog: View { Divider() ScrollView { if selectedRoute != nil { - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { + if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 > 1 { Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { From 65a5b3adc88deeebfddf35d7aa2341836b91adc6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Oct 2024 18:18:45 -0700 Subject: [PATCH 268/333] Detection sensor trigger types --- Localizable.xcstrings | 3 ++ Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Enums/DetectionSensorEnums.swift | 53 +++++++++++++++++++ .../contents | 2 +- Meshtastic/Persistence/UpdateCoreData.swift | 4 +- .../Config/Module/DetectionSensorConfig.swift | 22 ++++---- 6 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 Meshtastic/Enums/DetectionSensorEnums.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3a3c9a4b..2e76527e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -21872,6 +21872,9 @@ }, "Treat double tap on supported accelerometers as a user button press." : { + }, + "TriggerType" : { + }, "Triple Click Ad Hoc Ping" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e870495b..c82b37b5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; }; DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; }; DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; + DD0BE3102CB9FDC4000BA445 /* DetectionSensorEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */ = {isa = PBXBuildFile; fileRef = DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */; }; DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; }; @@ -300,6 +301,7 @@ 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 = ""; }; 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 = ""; }; DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; @@ -797,6 +799,7 @@ DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */, DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */, + DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */, DDB6ABDF28B13AC700384BA1 /* DeviceEnums.swift */, DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */, DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */, @@ -1395,6 +1398,7 @@ DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */, + DD0BE3102CB9FDC4000BA445 /* DetectionSensorEnums.swift in Sources */, DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD97E96828EFE9A00056DDA4 /* About.swift in Sources */, diff --git a/Meshtastic/Enums/DetectionSensorEnums.swift b/Meshtastic/Enums/DetectionSensorEnums.swift new file mode 100644 index 00000000..34401a82 --- /dev/null +++ b/Meshtastic/Enums/DetectionSensorEnums.swift @@ -0,0 +1,53 @@ +// +// DetectionSensorEnums.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 10/11/24. +// +import MeshtasticProtobufs + +enum TriggerTypes: Int, CaseIterable, Identifiable { + + case logicLow = 0 + case logicHigh = 1 + case fallingEdge = 2 + case risingEdge = 3 + case eitherEdgeActiveLow = 4 + case eitherEdgeActiveHigh = 5 + + var id: Int { self.rawValue } + + var name: String { + switch self { + case .logicLow: + return "Low" + case .logicHigh: + return "High" + case .fallingEdge: + return "Falling Edge" + case .risingEdge: + return "Rising Edge" + case .eitherEdgeActiveLow: + return "Either Edge Low" + case .eitherEdgeActiveHigh: + return "Either Edge Hight" + } + } + func protoEnumValue() -> ModuleConfig.DetectionSensorConfig.TriggerType { + + switch self { + case .logicLow: + return ModuleConfig.DetectionSensorConfig.TriggerType.logicLow + case .logicHigh: + return ModuleConfig.DetectionSensorConfig.TriggerType.logicHigh + case .fallingEdge: + return ModuleConfig.DetectionSensorConfig.TriggerType.fallingEdge + case .risingEdge: + return ModuleConfig.DetectionSensorConfig.TriggerType.risingEdge + case .eitherEdgeActiveLow: + return ModuleConfig.DetectionSensorConfig.TriggerType.eitherEdgeActiveLow + case .eitherEdgeActiveHigh: + return ModuleConfig.DetectionSensorConfig.TriggerType.eitherEdgeActiveHigh + } + } +} diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents index 8ebdb9e2..bc188564 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents @@ -47,13 +47,13 @@ - + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 548ead43..6ddd664c 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -989,7 +989,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso newConfig.sendBell = config.sendBell newConfig.name = config.name newConfig.monitorPin = Int32(config.monitorPin) - newConfig.detectionTriggeredHigh = config.detectionTriggerType == .logicHigh ? true : false + newConfig.triggerType = Int32(config.detectionTriggerType.rawValue) newConfig.usePullup = config.usePullup newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) @@ -1000,7 +1000,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso fetchedNode[0].detectionSensorConfig?.name = config.name fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin) fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup - fetchedNode[0].detectionSensorConfig?.detectionTriggeredHigh = config.detectionTriggerType == .logicHigh ? true : false + fetchedNode[0].detectionSensorConfig?.triggerType = Int32(config.detectionTriggerType.rawValue) fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 076cb016..aff3dfea 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -36,7 +36,7 @@ struct DetectionSensorConfig: View { @State var enabled = false @State var sendBell: Bool = false @State var name: String = "" - @State var detectionTriggeredHigh: Bool = true + @State var triggerType = 0 @State var usePullup: Bool = false @State var minimumBroadcastSecs = 0 @State var stateBroadcastSecs = 0 @@ -116,11 +116,13 @@ struct DetectionSensorConfig: View { } .pickerStyle(DefaultPickerStyle()) -// Toggle(isOn: $detectionTriggeredHigh) { -// Label("Detection trigger High", systemImage: "dial.high") -// Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") -// } -// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Picker("TriggerType", selection: $triggerType) { + ForEach(TriggerTypes.allCases) { tt in + Text(tt.name).tag(tt.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) Toggle(isOn: $usePullup) { Label("Uses pullup resistor", systemImage: "arrow.up.to.line") @@ -166,7 +168,7 @@ struct DetectionSensorConfig: View { dsc.sendBell = self.sendBell dsc.name = self.name dsc.monitorPin = UInt32(self.monitorPin) - // dsc.detectionTriggeredHigh = self.detectionTriggeredHigh + dsc.detectionTriggerType = TriggerTypes(rawValue: triggerType)!.protoEnumValue() dsc.usePullup = self.usePullup dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs) dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs) @@ -216,8 +218,8 @@ struct DetectionSensorConfig: View { .onChange(of: sendBell) { _, newSendBell in if newSendBell != node?.detectionSensorConfig?.sendBell { hasChanges = true } } - .onChange(of: detectionTriggeredHigh) { _, newDetectionTriggeredHigh in - // if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true } + .onChange(of: triggerType) { _, newTriggerType in + if newTriggerType != node?.detectionSensorConfig?.triggerType ?? 0 { hasChanges = true } } .onChange(of: usePullup) { _, newUsePullup in if newUsePullup != node?.detectionSensorConfig?.usePullup { hasChanges = true } @@ -244,7 +246,7 @@ struct DetectionSensorConfig: View { self.name = (node?.detectionSensorConfig?.name ?? "") self.monitorPin = Int(node?.detectionSensorConfig?.monitorPin ?? 0) self.usePullup = (node?.detectionSensorConfig?.usePullup ?? false) - // self.detectionTriggeredHigh = (node?.detectionSensorConfig?.detectionTriggeredHigh ?? true) + self.triggerType = Int(node?.detectionSensorConfig?.triggerType ?? 0) self.minimumBroadcastSecs = Int(node?.detectionSensorConfig?.minimumBroadcastSecs ?? 45) self.stateBroadcastSecs = Int(node?.detectionSensorConfig?.stateBroadcastSecs ?? 0) From b66843b48dcaebec561c505984e9ac2e5aa323c0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Oct 2024 18:21:33 -0700 Subject: [PATCH 269/333] Set distance back again --- Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index bec035b7..36d9a915 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -24,7 +24,7 @@ struct NodeMapSwiftUI: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic - @State var distance = 0.0 + @State var distance = 10000.0 @State var scene: MKLookAroundScene? @State var isLookingAround = false @State var isShowingAltitude = false From 70263d1ea787a241ae3ed9343e0feb3bb3b267a9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 12 Oct 2024 05:35:14 -0700 Subject: [PATCH 270/333] update traceroute list text --- Localizable.xcstrings | 32 +++++++++++++++------- Meshtastic/Helpers/BLEManager.swift | 17 ++++++------ Meshtastic/Views/Nodes/TraceRouteLog.swift | 18 ++++++++++-- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2e76527e..65b72c6b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -27,16 +27,6 @@ }, "%@" : { - }, - "%@ - %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ - %2$@" - } - } - } }, "%@ - %@ - %@" : { "localizations" : { @@ -47,6 +37,28 @@ } } } + }, + "%@ - %d Hops Towards %d Hops Back" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$d Hops Towards %3$d Hops Back" + } + } + } + }, + "%@ - 1 Hop" : { + + }, + "%@ - Direct" : { + + }, + "%@ - No Response" : { + + }, + "%@ - Not Sent" : { + }, "%@ (%@)" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 86ecdc96..88b6fc1c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -854,7 +854,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } - var routeString = "\(connectedHop.name ?? "???") --> " + var routeString = "\(connectedNode.user?.longName ?? "???") --> " hopNodes.append(connectedHop) traceRoute?.hopsTowards = Int32(routingMessage.route.count) for (index, node) in routingMessage.route.enumerated() { @@ -887,16 +887,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized destinationHop.time = Date() + destinationHop.snr = traceRoute?.snr ?? 0.0 destinationHop.num = connectedPeripheral.num if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { - connectedHop.altitude = mostRecent.altitude - connectedHop.latitudeI = mostRecent.latitudeI - connectedHop.longitudeI = mostRecent.longitudeI + destinationHop.altitude = mostRecent.altitude + destinationHop.latitudeI = mostRecent.latitudeI + destinationHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } hopNodes.append(destinationHop) - routeString += "\(destinationHop.name ?? "???") (\(destinationHop.num.toHex()))" - var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) --> " + routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) (\((traceRoute?.node?.num ?? 0).toHex()))" + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) (\((traceRoute?.node?.num ?? 0).toHex()) --> " traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) @@ -926,7 +927,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNodes.append(traceRouteHop) routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } - routeBackString += "\(connectedNode.user?.longName ?? "???") (\(connectedNode.num.toHex()))" + routeBackString += "\(connectedNode.user?.longName ?? "unknown".localized) (\((connectedNode.num).toHex()))" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) @@ -937,7 +938,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } catch { context.rollback() let nsError = error as NSError - Logger.data.error("Error Updating Core Data TraceRouteHOp: \(nsError, privacy: .public)") + Logger.data.error("Error Updating Core Data TraceRouteHop: \(nsError, privacy: .public)") } let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) MeshLogger.log("🪧 \(logString)") diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index bc6a90f0..2ba7eb45 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -37,8 +37,22 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hopsTowards) \(route.hopsTowards == 1 ? "Hop": "Hops Towards \(route.hopsBack) Hops Back ")") : (route.sent ? "No Response" : "Not Sent"))") - .font(.caption) + if route.response && route.hops?.count == 0 { + Text("\(route.time?.formatted() ?? "unknown".localized) - Direct") + .font(.caption) + } else if route.response && route.hopsTowards == 1 { + Text("\(route.time?.formatted() ?? "unknown".localized) - 1 Hop") + .font(.caption) + } else if route.response { + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.hopsTowards) Hops Towards \(route.hopsBack) Hops Back") + .font(.caption) + } else if route.sent { + Text("\(route.time?.formatted() ?? "unknown".localized) - No Response") + .font(.caption) + } else { + Text("\(route.time?.formatted() ?? "unknown".localized) - Not Sent") + .font(.caption) + } } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) From aa9c8bd8a325be477c686600a235463dedd6136f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 12 Oct 2024 06:15:17 -0700 Subject: [PATCH 271/333] set destination node num properly --- 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 88b6fc1c..b128f032 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -888,7 +888,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized destinationHop.time = Date() destinationHop.snr = traceRoute?.snr ?? 0.0 - destinationHop.num = connectedPeripheral.num + destinationHop.num = traceRoute?.node?.num ?? 0 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { destinationHop.altitude = mostRecent.altitude destinationHop.latitudeI = mostRecent.latitudeI From a74e7bcd061a576e7b970a5e9f4119ac55c027a2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 12 Oct 2024 09:48:38 -0700 Subject: [PATCH 272/333] SNR for connected and destination nodes --- Meshtastic/Helpers/BLEManager.swift | 12 ++++++++---- Meshtastic/Views/Messages/UserList.swift | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b128f032..4f89a110 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -848,6 +848,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedHop.time = Date() connectedHop.num = connectedPeripheral.num connectedHop.name = connectedNode.user?.longName ?? "???" + connectedHop.snr = Float(routingMessage.snrBack.last ?? 0 / 4) if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI @@ -887,7 +888,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized destinationHop.time = Date() - destinationHop.snr = traceRoute?.snr ?? 0.0 + destinationHop.snr = Float(routingMessage.snrTowards.last ?? 0 / 4) destinationHop.num = traceRoute?.node?.num ?? 0 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { destinationHop.altitude = mostRecent.altitude @@ -896,8 +897,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute?.hasPositions = true } hopNodes.append(destinationHop) - routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) (\((traceRoute?.node?.num ?? 0).toHex()))" - var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) (\((traceRoute?.node?.num ?? 0).toHex()) --> " + routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB)" + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB) --> " traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) @@ -926,8 +927,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " + + + } - routeBackString += "\(connectedNode.user?.longName ?? "unknown".localized) (\((connectedNode.num).toHex()))" + routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) \(connectedNode.snr > 0 ? connectedNode.snr : 0.0)dB)" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4a4028e2..14c826ed 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -302,9 +302,13 @@ struct UserList: View { 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 { + /// Only show mqtt nodes that can be contacted (zero hops) on the default key + // let bothPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0 OR userNode.viaMqtt == NO") + // predicates.append(bothPredicate) } /// Roles if roleFilter && deviceRoles.count > 0 { From f0838eace92f2438b7e877ca07d12dd286da5744 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 13 Oct 2024 09:20:05 -0700 Subject: [PATCH 273/333] Update trace route list, only show traceroute icon if there are responses, temp layout test --- .../CoreData/NodeInfoEntityExtension.swift | 3 ++- Meshtastic/Helpers/BLEManager.swift | 4 +--- Meshtastic/Views/Layouts/TraceRoute.swift | 4 ++-- Meshtastic/Views/Nodes/TraceRouteLog.swift | 21 ++++++++++--------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 7585fb1e..7d313191 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -40,7 +40,8 @@ extension NodeInfoEntity { } var hasTraceRoutes: Bool { - return traceRoutes?.count ?? 0 > 0 + let routes = traceRoutes?.filter { ($0 as AnyObject).response } + return routes?.count ?? 0 > 0 } var hasPax: Bool { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4f89a110..4d6e2c75 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -897,6 +897,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute?.hasPositions = true } hopNodes.append(destinationHop) + /// Add the destination node to the end of the route towards string and the beginning of teh route back string routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB)" var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB) --> " traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) @@ -927,9 +928,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " - - - } routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) \(connectedNode.snr > 0 ? connectedNode.snr : 0.0)dB)" traceRoute?.routeText = routeString diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift index bb79f747..fd4d7d92 100644 --- a/Meshtastic/Views/Layouts/TraceRoute.swift +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -56,13 +56,13 @@ struct TraceRoute: Layout { subview.place(at: point, anchor: .center, proposal: .unspecified) - DispatchQueue.main.async { + // DispatchQueue.main.async { if index % 2 == 0 { subview[Rotation.self]?.wrappedValue = .zero } else { subview[Rotation.self]?.wrappedValue = .radians(angle) } - } + // } } } } diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 2ba7eb45..268055e2 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -37,7 +37,7 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - if route.response && route.hops?.count == 0 { + if route.response && route.hopsTowards == 0 { Text("\(route.time?.formatted() ?? "unknown".localized) - Direct") .font(.caption) } else if route.response && route.hopsTowards == 1 { @@ -76,7 +76,16 @@ struct TraceRouteLog: View { Divider() ScrollView { if selectedRoute != nil { - if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 > 1 { + + if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 == 0 { + Label { + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + .font(.title3) + } else if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 > 0 { Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { @@ -91,14 +100,6 @@ struct TraceRouteLog: View { .symbolRenderingMode(.hierarchical) } .font(.title3) - } else if selectedRoute?.response ?? false { - Label { - Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - } - .font(.title3) } else if !(selectedRoute?.sent ?? true) { Label { VStack { From c901e345b028b6f24d1b8414d3e70dbd6e59ba43 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sun, 13 Oct 2024 17:20:56 -0500 Subject: [PATCH 274/333] Enable Save button when position flags are modified --- .../Settings/Config/PositionConfig.swift | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index fe61d1fa..451ac02d 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -189,32 +189,49 @@ struct PositionConfig: View { Label("Altitude", systemImage: "arrow.up") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeAltitude) { _, newIncludeAltitude in + if newIncludeAltitude != PositionFlags(rawValue: self.positionFlags).contains(.Altitude) { hasChanges = true } + } Toggle(isOn: $includeSatsinview) { Label("Number of satellites", systemImage: "skew") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeSatsinview) { _, newIncludeSatsinview in + if newIncludeSatsinview != PositionFlags(rawValue: self.positionFlags).contains(.Satsinview) { hasChanges = true } + } Toggle(isOn: $includeSeqNo) { // 64 Label("Sequence number", systemImage: "number") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeSeqNo) { _, newIncludeSeqNo in + if newIncludeSeqNo != PositionFlags(rawValue: self.positionFlags).contains(.SeqNo) { hasChanges = true } + } Toggle(isOn: $includeTimestamp) { // 128 Label("timestamp", systemImage: "clock") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeTimestamp) { _, newIncludeTimestamp in + if newIncludeTimestamp != PositionFlags(rawValue: self.positionFlags).contains(.Timestamp) { hasChanges = true } + } Toggle(isOn: $includeHeading) { // 128 Label("Vehicle heading", systemImage: "location.circle") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeHeading) { _, newIncludeHeading in + if newIncludeHeading != PositionFlags(rawValue: self.positionFlags).contains(.Heading) { hasChanges = true } + } Toggle(isOn: $includeSpeed) { // 128 - Label("Vehicle speed", systemImage: "speedometer") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeSpeed) { _, newIncludeSpeed in + if newIncludeSpeed != PositionFlags(rawValue: self.positionFlags).contains(.Speed) { hasChanges = true } + } } } @@ -227,22 +244,35 @@ struct PositionConfig: View { Label("Altitude is Mean Sea Level", systemImage: "arrow.up.to.line.compact") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeAltitudeMsl) { _, newIncludeAltitudeMsl in + if newIncludeAltitudeMsl != PositionFlags(rawValue: self.positionFlags).contains(.AltitudeMsl) { hasChanges = true } + } + Toggle(isOn: $includeGeoidalSeparation) { Label("Altitude Geoidal Separation", systemImage: "globe.americas") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeGeoidalSeparation) { _, newIncludeGeoidalSeparation in + if newIncludeGeoidalSeparation != PositionFlags(rawValue: self.positionFlags).contains(.GeoidalSeparation) { hasChanges = true } + } } Toggle(isOn: $includeDop) { Text("Dilution of precision (DOP) PDOP used by default") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeDop) { _, newIncludeDop in + if newIncludeDop != PositionFlags(rawValue: self.positionFlags).contains(.Dop) { hasChanges = true } + } if includeDop { Toggle(isOn: $includeHvdop) { Text("If DOP is set, use HDOP / VDOP values instead of PDOP") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .onChange(of: includeHvdop) { _, newIncludeHvdop in + if newIncludeHvdop != PositionFlags(rawValue: self.positionFlags).contains(.Hvdop) { hasChanges = true } + } } } } From 2eb6ae08e93d3a8c7e294d9f5f4fe5514ae95d6b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 15 Oct 2024 19:54:48 -0700 Subject: [PATCH 275/333] Turn of wheel for deployment --- Meshtastic/Views/Nodes/TraceRouteLog.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 268055e2..45206cb0 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -131,7 +131,7 @@ struct TraceRouteLog: View { .symbolRenderingMode(.hierarchical) } } - if selectedRoute?.hops?.count ?? 0 >= 3 { + if false {//selectedRoute?.hops?.count ?? 0 >= 3 { HStack(alignment: .center) { GeometryReader { geometry in let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 45 : 85) From 90360489c5c9bf4c07b2438a038eb4a897bffdbf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 18 Oct 2024 21:45:02 -0700 Subject: [PATCH 276/333] Update feature.yml --- .github/ISSUE_TEMPLATE/feature.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 7da9628a..8ff162ff 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -30,6 +30,8 @@ body: attributes: label: Participation options: + - label: I am willing to sponsor this feature. + required: false - label: I am willing to submit a pull request for this issue. required: false - type: textarea From 178f675f754edf5177995ef9871c967efd30661d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 18 Oct 2024 21:45:17 -0700 Subject: [PATCH 277/333] Update feature.yml --- .github/ISSUE_TEMPLATE/feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 8ff162ff..67f2bcd6 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -30,7 +30,7 @@ body: attributes: label: Participation options: - - label: I am willing to sponsor this feature. + - label: I am willing to sponsor this feature. required: false - label: I am willing to submit a pull request for this issue. required: false From c33ae052c132c203841144c05c813cf32b90c724 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 18 Oct 2024 21:49:47 -0700 Subject: [PATCH 278/333] Update feature.yml --- .github/ISSUE_TEMPLATE/feature.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 67f2bcd6..66d9e8ab 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -32,8 +32,9 @@ body: options: - label: I am willing to sponsor this feature. required: false - - label: I am willing to submit a pull request for this issue. + - label: I am willing to submit a pull request for this feature. required: false + description: (Features without participation go to the backlog.) - type: textarea attributes: label: Additional comments From bdd253f3506add95e7c038f80d26836fe1e29e86 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 18 Oct 2024 21:50:15 -0700 Subject: [PATCH 279/333] Update feature.yml --- .github/ISSUE_TEMPLATE/feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 66d9e8ab..a0b401a9 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -29,12 +29,12 @@ body: - type: checkboxes attributes: label: Participation + description: (Features without participation go to the backlog.) options: - label: I am willing to sponsor this feature. required: false - label: I am willing to submit a pull request for this feature. required: false - description: (Features without participation go to the backlog.) - type: textarea attributes: label: Additional comments From 5a20d8c873c1ecfabb6d07d3922896142ab01f3f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 18 Oct 2024 23:35:44 -0700 Subject: [PATCH 280/333] Update live activity with new metrics --- Meshtastic/Helpers/MeshPackets.swift | 3 ++ Widgets/WidgetsLiveActivity.swift | 71 +++++++++++++++------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 803c4dff..a5cfa7ba 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -726,6 +726,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx) telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx) telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) + telemetry.numRxDupe = Int32(truncatingIfNeeded: telemetryMessage.localStats.numRxDupe) + telemetry.numTxRelay = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelay) + telemetry.numTxRelayCanceled = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelayCanceled) telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) telemetry.metricsType = 4 diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index efc6ca11..7c6396a3 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -20,6 +20,9 @@ struct WidgetsLiveActivity: Widget { sentPackets: context.state.sentPackets, receivedPackets: context.state.receivedPackets, badReceivedPackets: context.state.badReceivedPackets, + dupeReceivedPackets: context.state.dupeReceivedPackets, + packetsSentRelay: context.state.packetsSentRelay, + packetsCanceledRelay: context.state.packetsCanceledRelay, nodesOnline: context.state.nodesOnline, totalNodes: context.state.totalNodes, timerRange: context.state.timerRange) @@ -28,33 +31,31 @@ struct WidgetsLiveActivity: Widget { } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Spacer() - Text("Mesh") - .font(.callout) - .fontWeight(.medium) - .foregroundStyle(.primary) - .padding(.bottom, 10) - .fixedSize() - Spacer() - } if context.state.totalNodes >= 100 { Text("100+ online") - .font(.caption) + .font(.callout) .foregroundStyle(.secondary) .fixedSize() } else { Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online") - .font(.caption) + .font(.callout) .foregroundStyle(.secondary) .fixedSize() } Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") - .font(.caption) + .font(.caption2) .foregroundStyle(.secondary) .fixedSize() Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") - .font(.caption) + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize() + Text("Sent: \(context.state.sentPackets)") + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize() + Text("Received: \(context.state.receivedPackets)") + .font(.caption2) .foregroundStyle(.secondary) .fixedSize() } @@ -63,32 +64,27 @@ struct WidgetsLiveActivity: Widget { .tint(Color("LightIndigo")) } DynamicIslandExpandedRegion(.trailing, priority: 1) { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Spacer() - Text("Packets") - .font(.callout) - .fontWeight(.medium) - .foregroundStyle(.primary) - .padding(.bottom, 10) - .fixedSize() - Spacer() - } - Text("Sent: \(context.state.sentPackets)") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize() - Text("Received: \(context.state.receivedPackets)") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize() + Spacer() Text("Bad: \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() + Text("Dupe: \(context.state.dupeReceivedPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Relayed: \(context.state.packetsSentRelay)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Relay Cancel: \(context.state.packetsCanceledRelay)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() } DynamicIslandExpandedRegion(.bottom) { Text("Last Heard: \(Date().formatted())") - .font(.caption) + .font(.caption2) .fontWeight(.medium) .foregroundStyle(.tint) .fixedSize() @@ -150,6 +146,9 @@ struct LiveActivityView: View { var sentPackets: UInt32 var receivedPackets: UInt32 var badReceivedPackets: UInt32 + var dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 var nodesOnline: UInt32 var totalNodes: UInt32 var timerRange: ClosedRange @@ -164,7 +163,8 @@ struct LiveActivityView: View { .aspectRatio(contentMode: .fit) .frame(minWidth: 25, idealWidth: 45, maxWidth: 55) Spacer() - NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) + NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, + dupeReceivedPackets: dupeReceivedPackets, packetsSentRelay: packetsSentRelay, packetsCanceledRelay: packetsCanceledRelay, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) Spacer() } .tint(.primary) @@ -185,6 +185,9 @@ struct NodeInfoView: View { var sentPackets: UInt32 var receivedPackets: UInt32 var badReceivedPackets: UInt32 + var dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 var nodesOnline: UInt32 var totalNodes: UInt32 var timerRange: ClosedRange From ad2eefcb50487030f01960812ba5f11a0f002c98 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 20 Oct 2024 08:48:36 -0700 Subject: [PATCH 281/333] Remove specific version numbers --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index de9af2a2..c0b76df3 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,7 @@ open Meshtastic.xcworkspace ### Supported Operating Systems -The last two operating system versions are supported. Currently that is 16 and 17. - -* iOS 16.6+ -* iPadOS 16.6+ -* macOS 13+ +The last two major operating system versions are supported. ### Code Standards From 4d897da1b556cc7bc37b2dc5914df64363abbb5b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 20 Oct 2024 08:49:58 -0700 Subject: [PATCH 282/333] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c82b37b5..6ea81c15 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1704,7 +1704,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.9; + MARKETING_VERSION = 2.5.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1738,7 +1738,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.9; + MARKETING_VERSION = 2.5.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1770,7 +1770,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.9; + MARKETING_VERSION = 2.5.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1803,7 +1803,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.9; + MARKETING_VERSION = 2.5.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/README.md b/README.md index c0b76df3..c9d083bd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ open Meshtastic.xcworkspace ### Supported Operating Systems -The last two major operating system versions are supported. +The last two major operating system versions are supported on iOS, iPadOS and macOS. ### Code Standards From f7630262039e697bbe0449bf3ebe9e2a4b092a69 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Tue, 22 Oct 2024 22:44:14 -0500 Subject: [PATCH 283/333] Use snr from routing message for single hop results rather than node Clean up snr float handling in traceroute paths --- Meshtastic/Helpers/BLEManager.swift | 12 ++++++------ Meshtastic/Views/Nodes/TraceRouteLog.swift | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 4d6e2c75..25fbd9a8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -835,8 +835,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true if routingMessage.route.count == 0 { - let snr = routingMessage.snrBack.count > 0 ? routingMessage.snrBack[0] / 4 : 0 - traceRoute?.snr = Float(snr) + let snr = routingMessage.snrBack.count > 0 ? Float(routingMessage.snrBack[0]) / 4 : 0.0 + traceRoute?.snr = snr let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("🪧 \(logString)") } else { @@ -848,7 +848,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedHop.time = Date() connectedHop.num = connectedPeripheral.num connectedHop.name = connectedNode.user?.longName ?? "???" - connectedHop.snr = Float(routingMessage.snrBack.last ?? 0 / 4) + connectedHop.snr = Float(routingMessage.snrBack.last ?? 0) / 4 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI @@ -866,7 +866,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() if routingMessage.snrTowards.count >= index + 1 { - traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) + traceRouteHop.snr = Float(routingMessage.snrTowards[index]) / 4 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { @@ -888,7 +888,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized destinationHop.time = Date() - destinationHop.snr = Float(routingMessage.snrTowards.last ?? 0 / 4) + destinationHop.snr = Float(routingMessage.snrTowards.last ?? 0) / 4 destinationHop.num = traceRoute?.node?.num ?? 0 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { destinationHop.altitude = mostRecent.altitude @@ -910,7 +910,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.time = Date() traceRouteHop.back = true if routingMessage.snrBack.count >= index + 1 { - traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) + traceRouteHop.snr = Float(routingMessage.snrBack[index]) / 4 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 45206cb0..e6e72841 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -72,14 +72,13 @@ struct TraceRouteLog: View { } .listStyle(.plain) } - .frame(minHeight: CGFloat(node.traceRoutes?.count ?? 0 * 40), maxHeight: 250) + .frame(minHeight: CGFloat((node.traceRoutes?.count ?? 0) * 40), maxHeight: 250) Divider() ScrollView { if selectedRoute != nil { - if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 == 0 { Label { - Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.snr ?? 0.0)) dB") } icon: { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) @@ -131,7 +130,7 @@ struct TraceRouteLog: View { .symbolRenderingMode(.hierarchical) } } - if false {//selectedRoute?.hops?.count ?? 0 >= 3 { + if false {// selectedRoute?.hops?.count ?? 0 >= 3 { HStack(alignment: .center) { GeometryReader { geometry in let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 45 : 85) From 994a3ff2f8ed1a239bf70193d2ee1e76bf3266ae Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Wed, 23 Oct 2024 01:14:08 -0500 Subject: [PATCH 284/333] Clean up formatting of route strings and update snr checks to support egative snr values --- Meshtastic/Helpers/BLEManager.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 25fbd9a8..8eed5fd3 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -867,6 +867,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.time = Date() if routingMessage.snrTowards.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrTowards[index]) / 4 + } else { + // If no snr in route, try last snr from node + traceRouteHop.snr = hopNode?.snr ?? 0 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { @@ -883,7 +886,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } hopNodes.append(traceRouteHop) - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr)dB) --> " } let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized @@ -898,8 +901,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(destinationHop) /// Add the destination node to the end of the route towards string and the beginning of teh route back string - routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB)" - var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB) --> " + routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(traceRoute?.node?.snr ?? traceRoute?.node?.snr ?? 0.0)dB)" + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(traceRoute?.node?.snr ?? traceRoute?.node?.snr ?? 0.0)dB) --> " traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) @@ -911,6 +914,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.back = true if routingMessage.snrBack.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrBack[index]) / 4 + } else { + // If no snr in route, try last snr from node + traceRouteHop.snr = hopNode?.snr ?? 0 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { @@ -927,9 +933,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } hopNodes.append(traceRouteHop) - routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " + routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr)dB) --> " } - routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) \(connectedNode.snr > 0 ? connectedNode.snr : 0.0)dB)" + routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex()))" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) From 9b34840e4b8d89ba11dfe5c34d8ec9a505558b24 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 26 Oct 2024 10:15:17 -0500 Subject: [PATCH 285/333] Update SNR checks. SNR value of -32 means unknown Add comments explaining the SNR handing in traceroute --- Meshtastic/Helpers/BLEManager.swift | 31 ++++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 8eed5fd3..7712dd38 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -835,7 +835,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true if routingMessage.route.count == 0 { - let snr = routingMessage.snrBack.count > 0 ? Float(routingMessage.snrBack[0]) / 4 : 0.0 + // Routing messages snr values are snr * 4 stored as an int + // If a traceroute snr value is unknown this field will contain INT8_MIN or -128 + // After converting to a float here, -32 is our unknown value. + let snr = routingMessage.snrBack.count > 0 ? (Float(routingMessage.snrBack[0]) / 4) : -32 traceRoute?.snr = snr let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("🪧 \(logString)") @@ -848,7 +851,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedHop.time = Date() connectedHop.num = connectedPeripheral.num connectedHop.name = connectedNode.user?.longName ?? "???" - connectedHop.snr = Float(routingMessage.snrBack.last ?? 0) / 4 + // If nil, set to unknown, INT8_MIN (-128) then divide by 4 + connectedHop.snr = Float(routingMessage.snrBack.last ?? -128) / 4 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI @@ -868,8 +872,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if routingMessage.snrTowards.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrTowards[index]) / 4 } else { - // If no snr in route, try last snr from node - traceRouteHop.snr = hopNode?.snr ?? 0 + // If no snr in route, set unknown + traceRouteHop.snr = -32 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { @@ -886,12 +890,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } hopNodes.append(traceRouteHop) - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr)dB) --> " + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr != -32 ? String(traceRouteHop.snr) : "unknown ".localized)dB) --> " } let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized destinationHop.time = Date() - destinationHop.snr = Float(routingMessage.snrTowards.last ?? 0) / 4 + // If nil, set to unknown, INT8_MIN (-128) then divide by 4 + destinationHop.snr = Float(routingMessage.snrTowards.last ?? -128) / 4 destinationHop.num = traceRoute?.node?.num ?? 0 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { destinationHop.altitude = mostRecent.altitude @@ -901,8 +906,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(destinationHop) /// Add the destination node to the end of the route towards string and the beginning of teh route back string - routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(traceRoute?.node?.snr ?? traceRoute?.node?.snr ?? 0.0)dB)" - var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(traceRoute?.node?.snr ?? traceRoute?.node?.snr ?? 0.0)dB) --> " + routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)" + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> " traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) @@ -915,8 +920,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if routingMessage.snrBack.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrBack[index]) / 4 } else { - // If no snr in route, try last snr from node - traceRouteHop.snr = hopNode?.snr ?? 0 + // If no snr in route, set to unknown + traceRouteHop.snr = -32 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { @@ -933,9 +938,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } hopNodes.append(traceRouteHop) - routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr)dB) --> " + routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr != -32 ? String(traceRouteHop.snr) : "unknown ".localized)dB) --> " } - routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex()))" + // If nil, set to unknown, INT8_MIN (-128) then divide by 4 + let snrBackLast = Float(routingMessage.snrBack.last ?? -128) / 4 + routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) (\(snrBackLast != -32 ? String(snrBackLast) : "unknown ".localized)dB)" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) From e3dac4e4bd3b572e8c8df7a24196ecfe4fa7794d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 30 Oct 2024 22:59:47 -0700 Subject: [PATCH 286/333] Channel form cleanup --- Meshtastic/Views/Settings/Channels/ChannelForm.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 72d6f58b..06a4a260 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -138,7 +138,7 @@ struct ChannelForm: View { } if positionsEnabled { - if channelKey != "AQ==" && channelRole > 0 { + if (channelKey != "AQ==" && channelKeySize > 1) && channelRole > 0 { VStack(alignment: .leading) { Toggle(isOn: $preciseLocation) { Label("Precise Location", systemImage: "scope") @@ -212,12 +212,11 @@ struct ChannelForm: View { } .onChange(of: preciseLocation) { _, loc in if loc == true { - if channelKey == "AQ==" { + if channelKey == "AQ==" || channelKeySize <= 1 { preciseLocation = false } else { positionPrecision = 32 } - positionPrecision = 32 } else { positionPrecision = 14 } From ae0a0f14e4be4912e36030dcbece08842e1b3392 Mon Sep 17 00:00:00 2001 From: rcarteraz Date: Thu, 31 Oct 2024 10:44:40 -0700 Subject: [PATCH 287/333] space --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 8ddd5816..f8d64c85 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -90,7 +90,7 @@ struct SecurityConfig: View { RoundedRectangle(cornerRadius: 10.0) .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) ) - Text("The tertiarypublic key authorized to send admin messages to this node.") + Text("The tertiary public key authorized to send admin messages to this node.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } From b01bae8b6bbcf3fd38fbbabba2689e8d7b6130c7 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Thu, 31 Oct 2024 20:53:58 -0500 Subject: [PATCH 288/333] Skip back route if hopStart not set and no snrBack --- Meshtastic/Helpers/BLEManager.swift | 78 +++++++++++++++-------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 7712dd38..13636774 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -907,44 +907,48 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate hopNodes.append(destinationHop) /// Add the destination node to the end of the route towards string and the beginning of teh route back string routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)" - var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> " - traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) - for (index, node) in routingMessage.routeBack.enumerated() { - var hopNode = getNodeInfo(id: Int64(node), context: context) - if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { - hopNode = createNodeInfo(num: Int64(node), context: context) - } - let traceRouteHop = TraceRouteHopEntity(context: context) - traceRouteHop.time = Date() - traceRouteHop.back = true - if routingMessage.snrBack.count >= index + 1 { - traceRouteHop.snr = Float(routingMessage.snrBack[index]) / 4 - } else { - // If no snr in route, set to unknown - traceRouteHop.snr = -32 - } - if let hn = hopNode, hn.hasPositions { - if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { - traceRouteHop.altitude = mostRecent.altitude - traceRouteHop.latitudeI = mostRecent.latitudeI - traceRouteHop.longitudeI = mostRecent.longitudeI - traceRoute?.hasPositions = true - } - } - traceRouteHop.num = hopNode?.num ?? 0 - if hopNode != nil { - if decodedInfo.packet.rxTime > 0 { - hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) - } - } - hopNodes.append(traceRouteHop) - routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr != -32 ? String(traceRouteHop.snr) : "unknown ".localized)dB) --> " - } - // If nil, set to unknown, INT8_MIN (-128) then divide by 4 - let snrBackLast = Float(routingMessage.snrBack.last ?? -128) / 4 - routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) (\(snrBackLast != -32 ? String(snrBackLast) : "unknown ".localized)dB)" traceRoute?.routeText = routeString - traceRoute?.routeBackText = routeBackString + + traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) + // Only if hopStart is set and there is an SNR entry + if decodedInfo.packet.hopStart > 0 && routingMessage.snrBack.count > 0 { + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> " + for (index, node) in routingMessage.routeBack.enumerated() { + var hopNode = getNodeInfo(id: Int64(node), context: context) + if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { + hopNode = createNodeInfo(num: Int64(node), context: context) + } + let traceRouteHop = TraceRouteHopEntity(context: context) + traceRouteHop.time = Date() + traceRouteHop.back = true + if routingMessage.snrBack.count >= index + 1 { + traceRouteHop.snr = Float(routingMessage.snrBack[index]) / 4 + } else { + // If no snr in route, set to unknown + traceRouteHop.snr = -32 + } + if let hn = hopNode, hn.hasPositions { + if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + traceRouteHop.altitude = mostRecent.altitude + traceRouteHop.latitudeI = mostRecent.latitudeI + traceRouteHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + } + traceRouteHop.num = hopNode?.num ?? 0 + if hopNode != nil { + if decodedInfo.packet.rxTime > 0 { + hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) + } + } + hopNodes.append(traceRouteHop) + routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr != -32 ? String(traceRouteHop.snr) : "unknown ".localized)dB) --> " + } + // If nil, set to unknown, INT8_MIN (-128) then divide by 4 + let snrBackLast = Float(routingMessage.snrBack.last ?? -128) / 4 + routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) (\(snrBackLast != -32 ? String(snrBackLast) : "unknown ".localized)dB)" + traceRoute?.routeBackText = routeBackString + } traceRoute?.hops = NSOrderedSet(array: hopNodes) traceRoute?.time = Date() do { From 9666c007795a581610b79f7bb73be316327bd898 Mon Sep 17 00:00:00 2001 From: Beat Rupp Date: Tue, 5 Nov 2024 17:01:30 +0100 Subject: [PATCH 289/333] Update Localizable.xcstrings --- Localizable.xcstrings | 2323 ++++++++++++++++++++++++++++++++--------- 1 file changed, 1841 insertions(+), 482 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 65b72c6b..2881b290 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -55,10 +55,24 @@ }, "%@ - No Response" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Keine Antwort" + } + } + } }, "%@ - Not Sent" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Nicht gesendet" + } + } + } }, "%@ (%@)" : { "localizations" : { @@ -91,7 +105,14 @@ } }, "%@ away" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ entfernt" + } + } + } }, "%@ can be up to %@ bytes long." : { "localizations" : { @@ -154,7 +175,14 @@ }, "%lld or less hops away" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld oder weniger Hops entfernt" + } + } + } }, "%lld Readings Total" : { @@ -166,10 +194,24 @@ }, "%llddb Transmit Power" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb Übertragungsleistung" + } + } + } }, "%llddBm Transmit Power" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddBm Übertragungsleistung" + } + } + } }, "< 1%" : { @@ -327,7 +369,14 @@ } }, "Accuracy %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genauigkeit %@" + } + } + } }, "Ack SNR: %@ dB" : { @@ -339,17 +388,31 @@ }, "Actions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktionen" + } + } + } }, "Active" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiv" + } + } + } }, "activity" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Activity" + "value" : "Aktivität" } }, "en" : { @@ -403,7 +466,14 @@ } }, "Activity" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivität" + } + } + } }, "Add Channel" : { @@ -412,7 +482,14 @@ }, "Add to favorites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu Favoriten hinzufügen" + } + } + } }, "Additional help" : { @@ -701,7 +778,14 @@ }, "All" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle" + } + } + } }, "All device and app data will be deleted." : { @@ -716,10 +800,24 @@ }, "Altitude" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Höhe" + } + } + } }, "Altitude %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Höhe %@" + } + } + } }, "Altitude Geoidal Separation" : { @@ -728,7 +826,14 @@ }, "Always point north" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immer nach Norden zeigen" + } + } + } }, "always.on" : { "extractionState" : "migrated", @@ -906,7 +1011,14 @@ } }, "An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ein quelloffenes, netzunabhängiges, dezentrales Mesh-Netzwerk, das auf kostengünstigen, stromsparenden Funkgeräten läuft." + } + } + } }, "Any missed messages will be delivered again." : { @@ -924,7 +1036,14 @@ }, "Approximate Location" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ungefährer Standort" + } + } + } }, "appsettings" : { "localizations" : { @@ -990,7 +1109,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "New Node Notifications" + "value" : "Mitteilungen über neue Knoten" } }, "en" : { @@ -1165,7 +1284,14 @@ }, "Are you sure you want to factory reset the node?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher dass du den Knoten auf die Werkseinstellungen zurücksetzen willst?" + } + } + } }, "are.you.sure" : { "localizations" : { @@ -1231,7 +1357,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "ASCII fähig" + "value" : "ASCII-fähig" } }, "en" : { @@ -1414,7 +1540,14 @@ }, "Bandwidth" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bandbreite" + } + } + } }, "Bar" : { @@ -1560,7 +1693,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "%@ Die App wird automatisch zum präferierten Gerät wiederverbinden, sobald es in Reichweite kommt." + "value" : "%@ Die App wird automatisch wieder zum präferierten Gerät verbinden, sobald es in Reichweite kommt." } }, "en" : { @@ -1619,7 +1752,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "%@ Dieser fehler kann üblicherweise behoben werden, in dem man unter Einstellungen > Bluetooth die Verbindung manuell löscht und sich erneut mit dem Gerät verbindet." + "value" : "%@ Dieser Fehler kann üblicherweise behoben werden, indem man unter Einstellungen > Bluetooth die Verbindung manuell löscht und sich erneut mit dem Gerät verbindet." } }, "en" : { @@ -1678,7 +1811,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "%@ Bitte versuche es erneut. achte sorgfältig auf die richtige PIN." + "value" : "%@ Bitte versuche es erneut. Achte sorgfältig auf die richtige PIN." } }, "en" : { @@ -2391,7 +2524,14 @@ } }, "Cancel" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abbrechen" + } + } + } }, "canned.messages" : { "localizations" : { @@ -2690,7 +2830,14 @@ }, "Categories" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kategorien" + } + } + } }, "channel" : { "localizations" : { @@ -2751,7 +2898,14 @@ } }, "Channel" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kanal" + } + } + } }, "Channel 0 Included" : { @@ -3287,7 +3441,14 @@ }, "Color" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Farbe" + } + } + } }, "communicating" : { "localizations" : { @@ -3714,12 +3875,6 @@ "config.power.ls.secs" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Light Sleep Interval" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -3767,12 +3922,6 @@ "config.power.min.wake.secs" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Minimum Wake Interval" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -3822,7 +3971,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Power Saving" + "value" : "Stromsparen" } }, "en" : { @@ -3871,12 +4020,6 @@ }, "config.power.saving.description" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button." - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -3926,7 +4069,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Battery" + "value" : "Batterie" } }, "en" : { @@ -3979,7 +4122,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sleep" + "value" : "Schlafmodus" } }, "en" : { @@ -4031,7 +4174,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Power" + "value" : "Strom" } }, "en" : { @@ -4083,7 +4226,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "After" + "value" : "Nach" } }, "en" : { @@ -4135,7 +4278,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Shutdown on Power Loss" + "value" : "Herunterfahren bei Stromunterbruch" } }, "en" : { @@ -4187,7 +4330,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Power Config" + "value" : "Stromkonfiguration" } }, "en" : { @@ -4240,7 +4383,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth Off After" + "value" : "Bluetooth Aus nach" } }, "en" : { @@ -4293,7 +4436,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "RTTTL Ringtone" + "value" : "RTTTL Klingelton" } }, "en" : { @@ -4342,12 +4485,6 @@ }, "config.ringtone.description" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications." - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -4394,12 +4531,6 @@ }, "config.ringtone.label" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ringtone Transfer Language" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -4449,7 +4580,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ringtone Config" + "value" : "Klingelton Konfiguration" } }, "en" : { @@ -4501,7 +4632,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nach dem ändern der Einstellungen wird das Gerät neu starten." + "value" : "Nach dem Ändern der Einstellungen wird das Gerät neu starten." } }, "en" : { @@ -4561,10 +4692,24 @@ }, "Configure" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurieren" + } + } + } }, "Connect to a Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbunden mit einem Knoten" + } + } + } }, "connected" : { "localizations" : { @@ -4625,7 +4770,14 @@ } }, "Connected Node %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbunden mit Knoten %@" + } + } + } }, "connected.radio" : { "localizations" : { @@ -4744,7 +4896,14 @@ } }, "Connection Attempt %lld of 10" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindungsversuch %lld von 10" + } + } + } }, "contacts" : { "extractionState" : "manual", @@ -4871,13 +5030,33 @@ }, "Convex Hull" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konvexe Hülle" + } + } + } }, "Coordinate" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koordinate" + } + } + } }, "Coordinate %@, %@" : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koordinate %1$@, %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", @@ -4887,7 +5066,14 @@ } }, "Coordinates:" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koordinaten:" + } + } + } }, "copy" : { "localizations" : { @@ -4948,16 +5134,37 @@ } }, "Could not find node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten nicht gefunden" + } + } + } }, "Counter Clockwise Rotary Event" : { }, "Create Waypoint" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wegpunkt erstellen" + } + } + } }, "Created: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erstellt: %@" + } + } + } }, "current" : { "extractionState" : "stale", @@ -4965,7 +5172,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Current" + "value" : "Aktuell" } }, "en" : { @@ -5019,10 +5226,23 @@ } }, "Current Firmware Version: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelle Firmware Version: %@" + } + } + } }, "Current Firmware Version: %@, Latest Firmware Version: %@" : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelle Firmware Version: %1$@, neuste Firmware Version %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", @@ -5032,13 +5252,27 @@ } }, "Current: %lld" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuell: %lld" + } + } + } }, "Currently the reccomended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE." : { }, "Date" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datum" + } + } + } }, "Debug" : { @@ -5109,7 +5343,14 @@ } }, "Default" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard" + } + } + } }, "delete" : { "localizations" : { @@ -5185,10 +5426,24 @@ }, "Delete Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten löschen" + } + } + } }, "Delete Node?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten löschen?" + } + } + } }, "Description" : { @@ -5396,7 +5651,14 @@ } }, "Device GPS" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geräte-GPS" + } + } + } }, "Device is managed by a mesh administrator, the user is unable to access any of the device settings." : { @@ -5408,7 +5670,14 @@ }, "Device Model: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerätemodell: %@" + } + } + } }, "Device Role" : { @@ -5479,7 +5748,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Device Configuration" + "value" : "Gerätekonfiguration" } }, "en" : { @@ -5528,12 +5797,6 @@ }, "device.metrics.delete" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete all device metrics?" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -5766,7 +6029,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Client Leise - Das selbe wie Client, außer das die Pakete nicht über diesen Node weitergeleitet werden. Nimmt nicht am Mesh-Routing teil." + "value" : "Dasselbe wie Client, außer dass die Pakete nicht über diesen Knoten weitergeleitet werden. Nimmt nicht am Mesh-Routing teil." } }, "en" : { @@ -5883,8 +6146,8 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role." + "state" : "translated", + "value" : "Infrastruktur-Knoten nur auf einem Turm oder einer Bergspitze. Nicht für Dächer oder mobile Knoten verwenden. Übermittelt Nachrichten mit minimalem Mehraufwand. Nicht sichtbar in der Knotenliste." } }, "en" : { @@ -5942,8 +6205,8 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." + "state" : "translated", + "value" : "Router - Mesh Pakete werden bevorzugt über diesen Knoten gerouted. Dieser Knoten wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." } }, "en" : { @@ -6001,8 +6264,8 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Router Client - Mesh Pakete werden bevorzugt über diesen Node gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden." + "state" : "translated", + "value" : "Router Client - Mesh Pakete werden bevorzugt über diesen Knoten gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden." } }, "en" : { @@ -6295,7 +6558,14 @@ }, "Direct" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Direkt" + } + } + } }, "Direct Message Help" : { @@ -6365,7 +6635,14 @@ } }, "Disabled" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschaltet" + } + } + } }, "disconnect" : { "localizations" : { @@ -6430,7 +6707,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dismiss Keyboard" + "value" : "Tastatur ausblenden" } }, "en" : { @@ -6552,12 +6829,6 @@ }, "display.config" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Display Config" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -6668,7 +6939,14 @@ } }, "Distance" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distanz" + } + } + } }, "Documentation" : { @@ -6958,28 +7236,63 @@ } }, "Encrypted" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschlüsselt" + } + } + } }, "Encryption Enabled" : { }, "Enter DFU Mode" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DFÜ-Modus aktivieren" + } + } + } }, "environment" : { }, "Environment" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umgebung" + } + } + } }, "Environment Metrics Log" : { }, "Erase all app data?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle App-Daten löschen?" + } + } + } }, "Erase all device and app data?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Geräte- und App-Daten löschen?" + } + } + } }, "Error: %@" : { @@ -7180,10 +7493,24 @@ } }, "Factory Reset" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Werkseinstellungen" + } + } + } }, "Factory reset your device and app? " : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerät und App auf Werkseinstellungen zurücksetzen?" + } + } + } }, "Failed to encode message content" : { @@ -7195,16 +7522,37 @@ }, "Favorite" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorit" + } + } + } }, "Favorites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriten" + } + } + } }, "Favorites and nodes with recent messages show up at the top of the contact list." : { }, "Fetch the latest position of a cetain node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Letzte Position eines Knotens holen" + } + } + } }, "Fifteen Minutes" : { @@ -7213,17 +7561,31 @@ }, "Find a contact" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontakt suchen" + } + } + } }, "Find a node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Knoten finden" + } + } + } }, "finish" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Finish" + "value" : "Beenden" } }, "en" : { @@ -7277,13 +7639,27 @@ } }, "Firmware" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmware" + } + } + } }, "Firmware update docs" : { }, "Firmware Updates" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmwareaktualisierungen" + } + } + } }, "firmware.version" : { "localizations" : { @@ -7406,7 +7782,14 @@ }, "Five Minutes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fünf Minuten" + } + } + } }, "Fixed Position" : { @@ -7421,13 +7804,34 @@ }, "For everyone" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Für alle" + } + } + } }, "For me" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Für mich" + } + } + } }, "Frequency" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frequenz" + } + } + } }, "Frequency Override" : { @@ -7503,12 +7907,6 @@ "gas.resistance" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gas Resistance" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -7621,7 +8019,14 @@ }, "Get Node Position" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotenposition ermitteln" + } + } + } }, "Get NRF DFU from the App Store" : { @@ -8019,6 +8424,12 @@ "gpsmode.disabled" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschaltet" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -8054,6 +8465,12 @@ "gpsmode.enabled" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eingeschaltet" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -8122,7 +8539,14 @@ } }, "Group Message" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppennachricht" + } + } + } }, "Gusts %@" : { @@ -8264,7 +8688,14 @@ }, "HIGH" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "HOCH" + } + } + } }, "History Return Max" : { @@ -8273,19 +8704,54 @@ }, "Hops Away" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt" + } + } + } }, "Hops Away %d" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt %d" + } + } + } }, "Hops Away:" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt:" + } + } + } }, "Hops Away: %d" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt: %d" + } + } + } }, "Hour" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stunde" + } + } + } }, "Hourly Duty Cycle" : { @@ -8309,16 +8775,37 @@ }, "How to update Firmware" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wie wird die Firmware aktualisiert" + } + } + } }, "Hum" : { }, "Humidity" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luftfeuchtigkeit" + } + } + } }, "HUMIDITY" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "LUFTFEUCHTIGKEIT" + } + } + } }, "hybrid" : { "extractionState" : "migrated", @@ -8535,7 +9022,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Incomplete" + "value" : "Unvollständig" } }, "en" : { @@ -9069,7 +9556,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Eighteen Hours" + "value" : "Achtzehn Stunden" } }, "en" : { @@ -9145,7 +9632,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fünfzehn Minutes" + "value" : "Fünfzehn Minuten" } }, "en" : { @@ -9263,7 +9750,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Five Hours" + "value" : "Fünf Stunden" } }, "en" : { @@ -9322,7 +9809,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fünf Minutes" + "value" : "Fünf Minuten" } }, "en" : { @@ -9440,7 +9927,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Forty Eight Hours Hours" + "value" : "Achtundvierzig Stunden" } }, "en" : { @@ -9487,7 +9974,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Forty Five Seconds" + "value" : "Fündundvierzig Sekunden" } }, "en" : { @@ -9546,7 +10033,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Four Hours" + "value" : "Vier Stunden" } }, "en" : { @@ -9605,7 +10092,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Four Seconds" + "value" : "Vier Sekunden" } }, "en" : { @@ -9841,7 +10328,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Seventy Two Hours" + "value" : "Zweiundsiebzig Stunden" } }, "en" : { @@ -9947,7 +10434,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Zehn Minutes" + "value" : "Zehn Minuten" } }, "en" : { @@ -10065,7 +10552,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dreißig Minutes" + "value" : "Dreißig Minuten" } }, "en" : { @@ -10183,7 +10670,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Thirty Six Hours" + "value" : "Sechsunddreissig Stunden" } }, "en" : { @@ -10242,7 +10729,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Three Hours" + "value" : "Drei Stunden" } }, "en" : { @@ -10301,7 +10788,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Three Seconds" + "value" : "Drei Sekunden" } }, "en" : { @@ -10596,7 +11083,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Two Hours" + "value" : "Zwei Stunden" } }, "en" : { @@ -10791,13 +11278,27 @@ }, "Key" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taste" + } + } + } }, "Key Mapping" : { }, "Key Size" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tastengröße" + } + } + } }, "keyboard.type" : { "extractionState" : "manual", @@ -10859,10 +11360,24 @@ } }, "Last heard" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zuletzt gehört" + } + } + } }, "Latitude" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Breitengrad" + } + } + } }, "LED Heartbeat" : { @@ -10886,13 +11401,34 @@ }, "Location" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standort" + } + } + } }, "Location:" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standrot:" + } + } + } }, "Locked" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesperrt" + } + } + } }, "Log Levels" : { @@ -10902,7 +11438,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Category" + "value" : "Kategorie" } }, "en" : { @@ -11006,7 +11542,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Message" + "value" : "Nachricht" } }, "en" : { @@ -11059,7 +11595,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Process" + "value" : "Prozess" } }, "en" : { @@ -11164,7 +11700,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Time" + "value" : "Zeit" } }, "en" : { @@ -11276,16 +11812,37 @@ }, "Long Name" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langer Name" + } + } + } }, "Long Name: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langer Name: %@" + } + } + } }, "Long press to favorite or mute the contact or delete a conversation." : { }, "Longitude" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Längengrad" + } + } + } }, "lora" : { "localizations" : { @@ -11468,7 +12025,14 @@ } }, "Map Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kartenoptionen" + } + } + } }, "Map Publish Interval" : { @@ -11482,12 +12046,6 @@ "map.centering" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Centering" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -11541,12 +12099,6 @@ "map.recentering" : { "extractionState" : "stale", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatic Re-centering" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -11600,12 +12152,6 @@ "map.tiles.delete" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Cached Map Tiles" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -11662,7 +12208,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "kartentyp" + "value" : "Kartentyp" } }, "en" : { @@ -11839,7 +12385,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Follow" + "value" : "Folgen" } }, "en" : { @@ -11898,7 +12444,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Follow with heading" + "value" : "Folgen mit Steuerkurs" } }, "en" : { @@ -11957,7 +12503,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "None" + "value" : "Keiner" } }, "en" : { @@ -12018,7 +12564,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Mesh Live Activity" + "value" : "Mesh Live Aktivität" } }, "en" : { @@ -12132,12 +12678,6 @@ "mesh.log.ambientlighting.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ambient Lighting module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12250,12 +12790,6 @@ "mesh.log.cannedmessage.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canned Message module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12309,12 +12843,6 @@ "mesh.log.cannedmessages.messages.get %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requested Canned Messages Module Messages for node: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12368,12 +12896,6 @@ "mesh.log.cannedmessages.messages.received %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canned Messages Messages Received For: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12424,74 +12946,9 @@ } } }, - "mesh.log.channel.received %d %@" : { - "extractionState" : "migrated", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Channel %d received from: %@" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Channel %d received from: %@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal %d reçu de : %@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ערוץ %d התקבל מ-%@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odebrano kanał %d od: %@" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canal %d recebido de: %@" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kanal %d mottagen från: %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Channel %d received from: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "Channel %d received from: %@" - } - } - } - }, "mesh.log.channel.sent %@ %d" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sent a Channel for: %@ Channel Index %d" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12545,12 +13002,6 @@ "mesh.log.detectionsensor.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detection Sensor module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12607,7 +13058,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Geräte Konfiguration empfangen: %@" + "value" : "Gerätekonfiguration empfangen: %@" } }, "en" : { @@ -12666,7 +13117,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Device Metadata received from: %@" + "value" : "Device Metadata empfangen von: %@" } }, "en" : { @@ -12840,12 +13291,6 @@ "mesh.log.externalnotification.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "External Notification module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12902,7 +13347,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "LoRa config received: %@" + "value" : "LoRa config empfangen: %@" } }, "en" : { @@ -12961,7 +13406,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a LoRa.Config for: %@" + "value" : "LoRa.Config gesendet für: %@" } }, "en" : { @@ -13020,7 +13465,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "MQTT module config received: %@" + "value" : "MQTT Modulkonfiguration empfangen: %@" } }, "en" : { @@ -13079,7 +13524,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "MyInfo received: %@" + "value" : "MyInfo empfangen: %@" } }, "en" : { @@ -13138,7 +13583,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Netzwerk onfiguration empfangen: %@" + "value" : "Netzwerkkonfiguration empfangen: %@" } }, "en" : { @@ -13197,7 +13642,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Node info empfangen für: %@" + "value" : "Knoteninformation empfangen für: %@" } }, "en" : { @@ -13332,7 +13777,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Positions Konfiguration empfangen: %@" + "value" : "Positionskonfiguration empfangen: %@" } }, "en" : { @@ -13391,7 +13836,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Positionspaket empfangen von Node: %@" + "value" : "Position empfangen von Knoten: %@" } }, "en" : { @@ -13532,7 +13977,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "RTTTL Ringtone config received: %@" + "value" : "RTTTL Klingeltonkonfiguration empfangen: %@" } }, "en" : { @@ -13709,7 +14154,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Position Packet from the Apple device GPS to node: %@" + "value" : "Position von Apple Gerät an Knoten gesendet: %@" } }, "en" : { @@ -13765,12 +14210,6 @@ "mesh.log.storeforward.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Store & Forward module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -14122,7 +14561,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Traceroute Anforderung an node gesendet: %@ wurde direkt empfangen." + "value" : "Traceroute Anforderung an Knoten gesendet: %@ wurde direkt empfangen." } }, "en" : { @@ -14240,7 +14679,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sende Traceroute Anforderung zu Mode: %@" + "value" : "Sende Traceroute Anforderung zu Knoten: %@" } }, "en" : { @@ -14296,12 +14735,6 @@ "mesh.log.wantconfig %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Issuing Want Config to %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -14358,7 +14791,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Waypoint Packet received from node: %@" + "value" : "Wegpunkt von Knoten empfangen: %@" } }, "en" : { @@ -14417,7 +14850,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Waypoint Packet from: %@" + "value" : "Wegpunkt gesendet von: %@" } }, "en" : { @@ -14471,7 +14904,14 @@ } }, "Meshtastic Node %@ has shared channels with you" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic Knoten %@ hat Kanäle mit dir geteilt" + } + } + } }, "Meshtastic® Copyright Meshtastic LLC" : { @@ -14535,10 +14975,24 @@ } }, "Message" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht" + } + } + } }, "Message content exceeds 200 bytes." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichteninhalt überschreitet 200 Bytes." + } + } + } }, "Message Status Options" : { @@ -14660,16 +15114,44 @@ } }, "Messages" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten" + } + } + } }, "Messages separate with |" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten getrennt mit |" + } + } + } }, "Minimum Distance" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum Distanz" + } + } + } }, "Minimum Interval" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum Intervall" + } + } + } }, "Minimum time between detection broadcasts" : { @@ -14917,7 +15399,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "MQTT Config" + "value" : "MQTT Konfiguration" } }, "en" : { @@ -14976,7 +15458,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Connect to MQTT" + "value" : "Verbunden mit MQTT" } }, "en" : { @@ -15035,7 +15517,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Disconnect from MQTT" + "value" : "Trennen von MQTT" } }, "en" : { @@ -15211,10 +15693,24 @@ } }, "Name" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + } + } }, "Name must be less than 30 bytes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name muss kürzer als 30 Bytes sein" + } + } + } }, "Nearby Topics" : { @@ -15342,13 +15838,34 @@ } }, "Never" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie" + } + } + } }, "Newer firmware is available" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neuere Firmware ist verfügbar" + } + } + } }, "No Connected Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kein verbundener Knoten" + } + } + } }, "No Device Metrics" : { @@ -15357,7 +15874,14 @@ }, "No Positions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Positionen" + } + } + } }, "no.nodes" : { "extractionState" : "manual", @@ -15365,7 +15889,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Meshtastic Nodes gefunden" + "value" : "Keine Meshtastic Knoten gefunden" } }, "en" : { @@ -15419,7 +15943,14 @@ } }, "Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten" + } + } + } }, "Node Core Data Backup %@/%@ - %@ - %@" : { "localizations" : { @@ -15432,19 +15963,47 @@ } }, "Node does not have positions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten hat keine Position" + } + } + } }, "Node History" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten Historie" + } + } + } }, "Node Info Broadcast Interval" : { }, "Node Map" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotenkarte" + } + } + } }, "Node Number" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotennummer" + } + } + } }, "nodelist.filter.distance %@" : { "extractionState" : "migrated", @@ -15452,7 +16011,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "up to %@ away" + "value" : "bis zu %@ entfernt" } }, "en" : { @@ -15510,7 +16069,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nodes" + "value" : "Knoten" } }, "en" : { @@ -15563,7 +16122,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nodes (%@)" + "value" : "Knoten (%@)" } }, "en" : { @@ -15678,19 +16237,54 @@ } }, "Notes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten" + } + } + } }, "Num: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl: %@" + } + } + } }, "Number of hops" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl Hops" + } + } + } }, "Number of records" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl Einträge" + } + } + } }, "Number of satellites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl Satelliten" + } + } + } }, "numbers.punctuation" : { "extractionState" : "manual", @@ -15870,13 +16464,27 @@ } }, "OK" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + } + } }, "Ok to MQTT" : { }, "OLED Type" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "OLED Typ" + } + } + } }, "on.boot" : { "extractionState" : "migrated", @@ -15941,16 +16549,44 @@ }, "One Hour" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine Stunde" + } + } + } }, "One Minute" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine Minute" + } + } + } }, "Online" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Online" + } + } + } }, "Open Settings" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen öffnen" + } + } + } }, "Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss" : { @@ -16017,7 +16653,14 @@ } }, "Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optionen" + } + } + } }, "OS Log Entry Details" : { @@ -16295,7 +16938,14 @@ } }, "Perform a factory reset on the node you are connected to" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbundenen Knoten auf Werkseinstellungen zurücksetzen" + } + } + } }, "phone.gps" : { "localizations" : { @@ -16424,7 +17074,14 @@ }, "PKI based node administration, requires firmware version 2.5+" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKI-basierte Knotenadministration, benötigt Firmware Version 2.5+" + } + } + } }, "Please connect to a radio to configure settings." : { @@ -16509,7 +17166,14 @@ }, "Position Sent" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Position gesendet" + } + } + } }, "position.config" : { "localizations" : { @@ -16572,6 +17236,12 @@ "position.precision %@" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Innerhalb %@" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -16611,10 +17281,24 @@ }, "Powered" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Angeschaltet" + } + } + } }, "Precise Location" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genaue Position" + } + } + } }, "preferred.radio" : { "extractionState" : "manual", @@ -16682,7 +17366,14 @@ }, "PRESSURE" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DRUCK" + } + } + } }, "Primary" : { @@ -16949,7 +17640,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Reboot" + "value" : "Neustart" } }, "en" : { @@ -17003,14 +17694,21 @@ } }, "Reboot Node?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten neustarten?" + } + } + } }, "reboot.node" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Node neustarten?" + "value" : "Knoten neustarten?" } }, "en" : { @@ -17186,17 +17884,37 @@ } }, "Recording route" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Route aufzeichnen" + } + } + } }, "Refresh device metadata" : { }, "Region" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Region" + } + } + } }, "relativetimeofday.afternoon" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachmittag" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17214,6 +17932,12 @@ "relativetimeofday.evening" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abend" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17231,6 +17955,12 @@ "relativetimeofday.midday" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mittag" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17248,6 +17978,12 @@ "relativetimeofday.morning" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Morgen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17265,6 +18001,12 @@ "relativetimeofday.nighttime" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nacht" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17292,10 +18034,24 @@ }, "Remove" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entfernen" + } + } + } }, "Remove from favorites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Von Favoriten entfernen" + } + } + } }, "Replace Channels" : { @@ -17368,26 +18124,61 @@ }, "Reset App Settings" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Einstellungen zurücksetzen" + } + } + } }, "Reset NodeDB" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotendatenbank zurücksetzen" + } + } + } }, "Restart" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neustarten" + } + } + } }, "Restart to the node you are connected to" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbundenen Knoten neustarten" + } + } + } }, "restore" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiederherstellen" + } + } + } }, "resume" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Resume" + "value" : "Fortsetzen" } }, "en" : { @@ -17441,14 +18232,21 @@ } }, "Review the app" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App bewerten" + } + } + } }, "ringtone" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ringtone" + "value" : "Klingelton" } }, "en" : { @@ -17507,7 +18305,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ringtone Config" + "value" : "Klingelton Konfiguration" } }, "en" : { @@ -17561,13 +18359,34 @@ } }, "Role" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rolle" + } + } + } }, "Role: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rolle: %@" + } + } + } }, "Roles" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rollen" + } + } + } }, "Root Topic" : { @@ -17585,7 +18404,14 @@ }, "Route: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Route: %@" + } + } + } }, "route.recorder" : { "localizations" : { @@ -17656,7 +18482,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Routes" + "value" : "Routen" } }, "en" : { @@ -17712,6 +18538,12 @@ "routes.activitytype.biking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biken" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17729,6 +18561,12 @@ "routes.activitytype.driving" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17746,6 +18584,12 @@ "routes.activitytype.filename.biking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "biken" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17763,6 +18607,12 @@ "routes.activitytype.filename.driving" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "fahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17780,6 +18630,12 @@ "routes.activitytype.filename.hiking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "wandern" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17814,6 +18670,12 @@ "routes.activitytype.filename.skiing" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "skitour" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17831,6 +18693,12 @@ "routes.activitytype.filename.walking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "gehen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17848,6 +18716,12 @@ "routes.activitytype.hiking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wandern" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17882,6 +18756,12 @@ "routes.activitytype.skiing" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skifahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17899,6 +18779,12 @@ "routes.activitytype.walking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gehen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -18273,7 +19159,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kein Interface" + "value" : "Keine Schnittstelle" } }, "en" : { @@ -18506,6 +19392,12 @@ "routing.pkifailed" : { "extractionState" : "manual", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschlüsseltes Senden fehlgeschlagen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -18517,6 +19409,12 @@ "routing.pkiunknownpubkey" : { "extractionState" : "manual", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbekannter öffentlicher Schlüssel" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -18774,13 +19672,34 @@ } }, "Sats" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satelliten" + } + } + } }, "Sats Estimate %lld" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satelliten Schätzung %lld" + } + } + } }, "Sats in view: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satelliten in Sicht: %@" + } + } + } }, "save" : { "localizations" : { @@ -18841,10 +19760,24 @@ } }, "Save" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Speichern" + } + } + } }, "Save User Config to %@?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benutzerkonfiguration nach %@ speichern?" + } + } + } }, "save.config %@" : { "extractionState" : "migrated", @@ -18852,7 +19785,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Save Config for %@" + "value" : "Speichere Konfiguration für %@" } }, "en" : { @@ -18912,7 +19845,14 @@ }, "Search" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suchen" + } + } + } }, "Second" : { @@ -18924,16 +19864,37 @@ }, "Security" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sicherheit" + } + } + } }, "Security Config" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sicherheitskonfiguration" + } + } + } }, "Security Config Settings require a firmware version 2.5+" : { }, "Select a channel" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kanal wählen" + } + } + } }, "Select a conversation" : { @@ -19067,7 +20028,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Node auswählen" + "value" : "Knoten auswählen" } }, "en" : { @@ -19121,13 +20082,34 @@ } }, "Send" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senden" + } + } + } }, "Send ${messageContent} to ${channelNumber}" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sende ${messageContent} an ${channelNumber}" + } + } + } }, "Send a Group Message" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppennachricht senden" + } + } + } }, "Send a message to a certain meshtastic channel" : { @@ -19136,16 +20118,37 @@ }, "Send a shutdown to the node you are connected to" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herunterfahren an verbundenen Knoten senden" + } + } + } }, "Send a Waypoint" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wegpunkt senden" + } + } + } }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { }, "Send Bell" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sende Glocke" + } + } + } }, "Send Reboot OTA" : { @@ -19166,10 +20169,24 @@ }, "Sequence number" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sequenznummer" + } + } + } }, "Sequence: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sequenz: %@" + } + } + } }, "serial" : { "localizations" : { @@ -19535,7 +20552,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Text Nachricht" + "value" : "Textnachricht" } }, "en" : { @@ -19589,10 +20606,24 @@ } }, "Server" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Server" + } + } + } }, "Server Address" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serveradresse" + } + } + } }, "Set" : { @@ -19720,7 +20751,14 @@ } }, "Share QR Code & Link" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR Code & Link teilen" + } + } + } }, "share.channels" : { "localizations" : { @@ -19840,40 +20878,124 @@ } }, "Shared Key" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemeinsamer Schlüssel" + } + } + } }, "Short Name" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurzname" + } + } + } }, "Short Name: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurzname: %@" + } + } + } }, "Show alerts" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Alarme" + } + } + } }, "Show Alerts" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Alarme" + } + } + } }, "Show nodes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Knoten" + } + } + } }, "Show on device screen" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige auf dem Gerätebildschirm" + } + } + } }, "Show on the mesh map." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige auf der Netzwerkkarte." + } + } + } }, "Show Waypoints " : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Wegpunkte" + } + } + } }, "Shut Down" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herunterfahren" + } + } + } }, "Shut Down Node?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten herunterfahren?" + } + } + } }, "Shutdown Node?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten herunterfahren?" + } + } + } }, "Signal %@" : { @@ -19894,13 +21016,34 @@ }, "Speed" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geschwindigkeit" + } + } + } }, "Speed %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geschwindigkeit %@" + } + } + } }, "Speed: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geschwindigkeit: %@" + } + } + } }, "Spread Factor" : { @@ -20269,7 +21412,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Send Heartbeat" + "value" : "Herzschlag senden" } }, "en" : { @@ -20327,7 +21470,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Subscribed to mesh" + "value" : "Verbunden mit dem Mesh" } }, "en" : { @@ -20381,7 +21524,14 @@ } }, "Supported" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterstützt" + } + } + } }, "Supported I2C Connected sensors will be detected automatically, sensors are BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 and SHTC3." : { @@ -20391,7 +21541,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tapback Response" + "value" : "Tapback Antwort" } }, "en" : { @@ -20568,7 +21718,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Gehört" + "value" : "Herz" } }, "en" : { @@ -20863,7 +22013,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wave" + "value" : "Welle" } }, "en" : { @@ -21033,13 +22183,34 @@ } }, "Temp" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temp" + } + } + } }, "Temperature" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatur" + } + } + } }, "Ten Minutes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zehn Minuten" + } + } + } }, "Tertiary Admin Key" : { @@ -21051,7 +22222,14 @@ }, "The dew point is %@ right now." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Taupunkt ist gerade %@" + } + } + } }, "The fastest that position updates will be sent if the minimum distance has been satisfied" : { @@ -21092,7 +22270,7 @@ "The state of the LED (on/off)" : { }, - "The tertiarypublic key authorized to send admin messages to this node." : { + "The tertiary public key authorized to send admin messages to this node." : { }, "There has been no response to a request for device metadata over the admin channel for this node." : { @@ -21102,7 +22280,14 @@ }, "Thirty Minutes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dreißig Minuten" + } + } + } }, "This conversation will be deleted." : { @@ -21120,7 +22305,14 @@ }, "This message was likely not delivered." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Nachricht wurde höchstwahrscheinlich nicht übermittelt." + } + } + } }, "This will disable fixed position and remove the currently set position." : { @@ -21129,16 +22321,44 @@ }, "Time" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeit" + } + } + } }, "Time Stamp" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitstempel" + } + } + } }, "Time Zone" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitzone" + } + } + } }, "Time zone for dates on the device screen and log." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitzone für Daten auf dem Gerätebildschirm und Log." + } + } + } }, "timeout" : { "localizations" : { @@ -21203,7 +22423,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Timestamp" + "value" : "Zeitstempel" } }, "en" : { @@ -21380,7 +22600,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Admin channel detected: Select a node from the drop down to manage connected or remote devices." + "value" : "Admin Kanal erkannt: Wähle einen Knoten vom Dropdown aus um verbundene oder entfernte Geräte zu verwalten." } }, "en" : { @@ -21438,7 +22658,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Admin Channel" + "value" : "Admin Kanal" } }, "en" : { @@ -21496,7 +22716,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)" + "value" : "Die meisten Daten in deinem Mesh werden über den primären Kanal gesendet. Du kannst sekundäre Kanäle einrichten, um zusätzliche Nachrichtengruppen zu erstellen, die durch ihren eigenen Schlüssel gesichert sind. [Tipps zur Kanalkonfiguration](https://meshtastic.org/docs/configuration/radio/channels/)" } }, "en" : { @@ -21554,7 +22774,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Manage Channels" + "value" : "Kanäle verwalten" } }, "en" : { @@ -21670,7 +22890,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sharing Meshtastic Channels" + "value" : "Meshtastic Kanäle teilen" } }, "en" : { @@ -21728,7 +22948,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details." + "value" : "Du kannst Kanalnachrichten (Gruppenchats) und Direktnachrichten senden und empfangen. Bei jeder Nachricht kannst du lange drücken, um verfügbare Aktionen wie Kopieren, Antworten, Tapback und Löschen sowie Zustelldetails anzuzeigen." } }, "en" : { @@ -21786,7 +23006,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Messages" + "value" : "Nachrichten" } }, "en" : { @@ -21843,7 +23063,14 @@ }, "Total" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + } + } }, "Trace Route" : { @@ -21874,7 +23101,14 @@ }, "Traffic" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verkehr" + } + } + } }, "Transmit data (txd) GPIO pin" : { @@ -21892,7 +23126,14 @@ }, "Try Again" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erneut versuchen" + } + } + } }, "twitter" : { "extractionState" : "manual", @@ -22150,7 +23391,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Update Your Firmware" + "value" : "Firmware aktualisieren" } }, "en" : { @@ -22208,7 +23449,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Update intervall" + "value" : "Aktualisierungsintervall" } }, "en" : { @@ -22265,7 +23506,14 @@ }, "Updated: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualisiert: %@" + } + } + } }, "Uplink Enabled" : { @@ -22446,10 +23694,24 @@ }, "Vehicle heading" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahrzeugsteuerkurs" + } + } + } }, "Vehicle speed" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahrzeuggeschwindigkeit" + } + } + } }, "Version %@ includes breaking changes to devices and the client apps. Only nodes version %@ and above are supported." : { "localizations" : { @@ -22466,6 +23728,12 @@ }, "Version: %@ (%@) " : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version: %1$@ (%2$@) " + } + }, "en" : { "stringUnit" : { "state" : "new", @@ -22475,10 +23743,24 @@ } }, "Via Lora" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via Lora" + } + } + } }, "Via Mqtt" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via Mqtt" + } + } + } }, "voltage" : { "localizations" : { @@ -22539,7 +23821,14 @@ } }, "Volts %@ " : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volt %@" + } + } + } }, "waiting" : { "localizations" : { @@ -22606,10 +23895,24 @@ }, "Waypoint Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wegpunktoptionen" + } + } + } }, "Weather Conditions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wetterverhältnisse" + } + } + } }, "Web Flasher" : { @@ -22618,10 +23921,24 @@ }, "What does the lock mean?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Was bedeutet das Schloß?" + } + } + } }, "What is Meshtastic?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Was ist Meshtastic?" + } + } + } }, "What licensed operator mode does:\n* Sets the node name to your call sign \n* Broadcasts node info every 10 minutes \n* Overrides frequency, dutycycle and tx power \n* Disables encryption" : { @@ -22630,16 +23947,44 @@ }, "WiFi Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFi Optionen" + } + } + } }, "WIND" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "WIND" + } + } + } }, "Wind Direction" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Windrichtung" + } + } + } }, "Wind Speed" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Windgeschwindigkeit" + } + } + } }, "x" : { @@ -22678,7 +24023,14 @@ }, "Yesterday" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestern" + } + } + } }, "You can also update your Meshtastic device over bluetooth using the Nordic DFU app." : { @@ -22687,7 +24039,14 @@ }, "Your Firmware is up to date" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Firmware ist aktuell" + } + } + } }, "Your MQTT Server must support TLS. Not available via the public mqtt server." : { @@ -22706,4 +24065,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} From 7dc8283eb6ac8b6836930407cb5c726920a3da0d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 5 Nov 2024 08:01:50 -0800 Subject: [PATCH 290/333] Update feature.yml --- .github/ISSUE_TEMPLATE/feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index a0b401a9..f0520eef 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -31,7 +31,7 @@ body: label: Participation description: (Features without participation go to the backlog.) options: - - label: I am willing to sponsor this feature. + - label: I am willing to pay to sponsor this feature. required: false - label: I am willing to submit a pull request for this feature. required: false From 64a5dfa4fe33a6bab139ae72bd71f8674908f343 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 6 Nov 2024 23:17:25 -0800 Subject: [PATCH 291/333] Create stale.yml --- .github/workflows/stale.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..3521f916 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,22 @@ +name: process stale Issues and PR's +on: + schedule: + - cron: 0 6 * * * + workflow_dispatch: {} + +permissions: + issues: write + pull-requests: write + actions: write + +jobs: + stale_issues: + name: Close Stale Issues + runs-on: ubuntu-latest + + steps: + - name: Stale PR+Issues + uses: actions/stale@v9.0.0 + with: + exempt-issue-labels: help wanted + exempt-pr-labels: From 5c508ad764b267c4e38edf65aff72b4bf947391f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Nov 2024 15:06:38 -0800 Subject: [PATCH 292/333] Update protobufs --- .../Sources/meshtastic/admin.pb.swift | 108 +++ .../Sources/meshtastic/config.pb.swift | 48 ++ .../Sources/meshtastic/device_ui.pb.swift | 718 ++++++++++++++++++ .../Sources/meshtastic/deviceonly.pb.swift | 207 +---- .../Sources/meshtastic/mesh.pb.swift | 226 +++++- .../Sources/meshtastic/module_config.pb.swift | 13 +- .../Sources/meshtastic/telemetry.pb.swift | 28 +- protobufs | 2 +- 8 files changed, 1138 insertions(+), 212 deletions(-) create mode 100644 MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 53d244d1..9e7fe0c5 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -386,6 +386,36 @@ public struct AdminMessage { set {payloadVariant = .setTimeOnly(newValue)} } + /// + /// Tell the node to send the stored ui data. + public var getUiConfigRequest: Bool { + get { + if case .getUiConfigRequest(let v)? = payloadVariant {return v} + return false + } + set {payloadVariant = .getUiConfigRequest(newValue)} + } + + /// + /// Reply stored device ui data. + public var getUiConfigResponse: DeviceUIConfig { + get { + if case .getUiConfigResponse(let v)? = payloadVariant {return v} + return DeviceUIConfig() + } + set {payloadVariant = .getUiConfigResponse(newValue)} + } + + /// + /// Tell the node to store UI data persistently. + public var storeUiConfig: DeviceUIConfig { + get { + if case .storeUiConfig(let v)? = payloadVariant {return v} + return DeviceUIConfig() + } + set {payloadVariant = .storeUiConfig(newValue)} + } + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) @@ -594,6 +624,15 @@ public struct AdminMessage { /// Convenience method to set the time on the node (as Net quality) without any other position data case setTimeOnly(UInt32) /// + /// Tell the node to send the stored ui data. + case getUiConfigRequest(Bool) + /// + /// Reply stored device ui data. + case getUiConfigResponse(DeviceUIConfig) + /// + /// Tell the node to store UI data persistently. + case storeUiConfig(DeviceUIConfig) + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) case beginEditSettings(Bool) @@ -766,6 +805,18 @@ public struct AdminMessage { guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() } return l == r }() + case (.getUiConfigRequest, .getUiConfigRequest): return { + guard case .getUiConfigRequest(let l) = lhs, case .getUiConfigRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getUiConfigResponse, .getUiConfigResponse): return { + guard case .getUiConfigResponse(let l) = lhs, case .getUiConfigResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.storeUiConfig, .storeUiConfig): return { + guard case .storeUiConfig(let l) = lhs, case .storeUiConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.beginEditSettings, .beginEditSettings): return { guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } return l == r @@ -845,6 +896,10 @@ public struct AdminMessage { /// TODO: REPLACE case securityConfig // = 7 case sessionkeyConfig // = 8 + + /// + /// device-ui config + case deviceuiConfig // = 9 case UNRECOGNIZED(Int) public init() { @@ -862,6 +917,7 @@ public struct AdminMessage { case 6: self = .bluetoothConfig case 7: self = .securityConfig case 8: self = .sessionkeyConfig + case 9: self = .deviceuiConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -877,6 +933,7 @@ public struct AdminMessage { case .bluetoothConfig: return 6 case .securityConfig: return 7 case .sessionkeyConfig: return 8 + case .deviceuiConfig: return 9 case .UNRECOGNIZED(let i): return i } } @@ -1002,6 +1059,7 @@ extension AdminMessage.ConfigType: CaseIterable { .bluetoothConfig, .securityConfig, .sessionkeyConfig, + .deviceuiConfig, ] } @@ -1123,6 +1181,9 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 41: .standard(proto: "set_fixed_position"), 42: .standard(proto: "remove_fixed_position"), 43: .standard(proto: "set_time_only"), + 44: .standard(proto: "get_ui_config_request"), + 45: .standard(proto: "get_ui_config_response"), + 46: .standard(proto: "store_ui_config"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), 94: .standard(proto: "factory_reset_device"), @@ -1477,6 +1538,40 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .setTimeOnly(v) } }() + case 44: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .getUiConfigRequest(v) + } + }() + case 45: try { + var v: DeviceUIConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .getUiConfigResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .getUiConfigResponse(v) + } + }() + case 46: try { + var v: DeviceUIConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .storeUiConfig(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .storeUiConfig(v) + } + }() case 64: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1697,6 +1792,18 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .setTimeOnly(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularFixed32Field(value: v, fieldNumber: 43) }() + case .getUiConfigRequest?: try { + guard case .getUiConfigRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 44) + }() + case .getUiConfigResponse?: try { + guard case .getUiConfigResponse(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 45) + }() + case .storeUiConfig?: try { + guard case .storeUiConfig(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 46) + }() case .beginEditSettings?: try { guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 64) @@ -1760,6 +1867,7 @@ extension AdminMessage.ConfigType: SwiftProtobuf._ProtoNameProviding { 6: .same(proto: "BLUETOOTH_CONFIG"), 7: .same(proto: "SECURITY_CONFIG"), 8: .same(proto: "SESSIONKEY_CONFIG"), + 9: .same(proto: "DEVICEUI_CONFIG"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 38df8ea0..49f405f1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -101,6 +101,14 @@ public struct Config { set {payloadVariant = .sessionkey(newValue)} } + public var deviceUi: DeviceUIConfig { + get { + if case .deviceUi(let v)? = payloadVariant {return v} + return DeviceUIConfig() + } + set {payloadVariant = .deviceUi(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -115,6 +123,7 @@ public struct Config { case bluetooth(Config.BluetoothConfig) case security(Config.SecurityConfig) case sessionkey(Config.SessionkeyConfig) + case deviceUi(DeviceUIConfig) #if !swift(>=4.1) public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { @@ -158,6 +167,10 @@ public struct Config { guard case .sessionkey(let l) = lhs, case .sessionkey(let r) = rhs else { preconditionFailure() } return l == r }() + case (.deviceUi, .deviceUi): return { + guard case .deviceUi(let l) = lhs, case .deviceUi(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -359,6 +372,15 @@ public struct Config { /// Ignores observed messages from foreign meshes like LOCAL_ONLY, /// but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB) case knownOnly // = 3 + + /// + /// Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. + case none // = 4 + + /// + /// Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. + /// Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. + case corePortnumsOnly // = 5 case UNRECOGNIZED(Int) public init() { @@ -371,6 +393,8 @@ public struct Config { case 1: self = .allSkipDecoding case 2: self = .localOnly case 3: self = .knownOnly + case 4: self = .none + case 5: self = .corePortnumsOnly default: self = .UNRECOGNIZED(rawValue) } } @@ -381,6 +405,8 @@ public struct Config { case .allSkipDecoding: return 1 case .localOnly: return 2 case .knownOnly: return 3 + case .none: return 4 + case .corePortnumsOnly: return 5 case .UNRECOGNIZED(let i): return i } } @@ -1652,6 +1678,8 @@ extension Config.DeviceConfig.RebroadcastMode: CaseIterable { .allSkipDecoding, .localOnly, .knownOnly, + .none, + .corePortnumsOnly, ] } @@ -1841,6 +1869,7 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 7: .same(proto: "bluetooth"), 8: .same(proto: "security"), 9: .same(proto: "sessionkey"), + 10: .standard(proto: "device_ui"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1966,6 +1995,19 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas self.payloadVariant = .sessionkey(v) } }() + case 10: try { + var v: DeviceUIConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .deviceUi(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .deviceUi(v) + } + }() default: break } } @@ -2013,6 +2055,10 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas guard case .sessionkey(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 9) }() + case .deviceUi?: try { + guard case .deviceUi(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2139,6 +2185,8 @@ extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding 1: .same(proto: "ALL_SKIP_DECODING"), 2: .same(proto: "LOCAL_ONLY"), 3: .same(proto: "KNOWN_ONLY"), + 4: .same(proto: "NONE"), + 5: .same(proto: "CORE_PORTNUMS_ONLY"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift new file mode 100644 index 00000000..f677d644 --- /dev/null +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -0,0 +1,718 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: meshtastic/device_ui.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +public enum Theme: SwiftProtobuf.Enum { + public typealias RawValue = Int + + /// + /// Dark + case dark // = 0 + + /// + /// Light + case light // = 1 + + /// + /// Red + case red // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .dark + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .dark + case 1: self = .light + case 2: self = .red + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .dark: return 0 + case .light: return 1 + case .red: return 2 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Theme: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Theme] = [ + .dark, + .light, + .red, + ] +} + +#endif // swift(>=4.2) + +/// +/// Localization +public enum Language: SwiftProtobuf.Enum { + public typealias RawValue = Int + + /// + /// English + case english // = 0 + + /// + /// French + case french // = 1 + + /// + /// German + case german // = 2 + + /// + /// Italian + case italian // = 3 + + /// + /// Portuguese + case portuguese // = 4 + + /// + /// Spanish + case spanish // = 5 + + /// + /// Swedish + case swedish // = 6 + + /// + /// Finnish + case finnish // = 7 + + /// + /// Polish + case polish // = 8 + + /// + /// Turkish + case turkish // = 9 + + /// + /// Serbian + case serbian // = 10 + + /// + /// Russian + case russian // = 11 + + /// + /// Dutch + case dutch // = 12 + + /// + /// Greek + case greek // = 13 + + /// + /// Simplified Chinese (experimental) + case simplifiedChinese // = 30 + + /// + /// Traditional Chinese (experimental) + case traditionalChinese // = 31 + case UNRECOGNIZED(Int) + + public init() { + self = .english + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .english + case 1: self = .french + case 2: self = .german + case 3: self = .italian + case 4: self = .portuguese + case 5: self = .spanish + case 6: self = .swedish + case 7: self = .finnish + case 8: self = .polish + case 9: self = .turkish + case 10: self = .serbian + case 11: self = .russian + case 12: self = .dutch + case 13: self = .greek + case 30: self = .simplifiedChinese + case 31: self = .traditionalChinese + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .english: return 0 + case .french: return 1 + case .german: return 2 + case .italian: return 3 + case .portuguese: return 4 + case .spanish: return 5 + case .swedish: return 6 + case .finnish: return 7 + case .polish: return 8 + case .turkish: return 9 + case .serbian: return 10 + case .russian: return 11 + case .dutch: return 12 + case .greek: return 13 + case .simplifiedChinese: return 30 + case .traditionalChinese: return 31 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Language: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Language] = [ + .english, + .french, + .german, + .italian, + .portuguese, + .spanish, + .swedish, + .finnish, + .polish, + .turkish, + .serbian, + .russian, + .dutch, + .greek, + .simplifiedChinese, + .traditionalChinese, + ] +} + +#endif // swift(>=4.2) + +public struct DeviceUIConfig { + // 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. + + /// + /// A version integer used to invalidate saved files when we make incompatible changes. + public var version: UInt32 { + get {return _storage._version} + set {_uniqueStorage()._version = newValue} + } + + /// + /// TFT display brightness 1..255 + public var screenBrightness: UInt32 { + get {return _storage._screenBrightness} + set {_uniqueStorage()._screenBrightness = newValue} + } + + /// + /// Screen timeout 0..900 + public var screenTimeout: UInt32 { + get {return _storage._screenTimeout} + set {_uniqueStorage()._screenTimeout = newValue} + } + + /// + /// Screen/Settings lock enabled + public var screenLock: Bool { + get {return _storage._screenLock} + set {_uniqueStorage()._screenLock = newValue} + } + + public var settingsLock: Bool { + get {return _storage._settingsLock} + set {_uniqueStorage()._settingsLock = newValue} + } + + public var pinCode: UInt32 { + get {return _storage._pinCode} + set {_uniqueStorage()._pinCode = newValue} + } + + /// + /// Color theme + public var theme: Theme { + get {return _storage._theme} + set {_uniqueStorage()._theme = newValue} + } + + /// + /// Audible message, banner and ring tone + public var alertEnabled: Bool { + get {return _storage._alertEnabled} + set {_uniqueStorage()._alertEnabled = newValue} + } + + public var bannerEnabled: Bool { + get {return _storage._bannerEnabled} + set {_uniqueStorage()._bannerEnabled = newValue} + } + + public var ringToneID: UInt32 { + get {return _storage._ringToneID} + set {_uniqueStorage()._ringToneID = newValue} + } + + /// + /// Localization + public var language: Language { + get {return _storage._language} + set {_uniqueStorage()._language = newValue} + } + + /// + /// Node list filter + public var nodeFilter: NodeFilter { + get {return _storage._nodeFilter ?? NodeFilter()} + set {_uniqueStorage()._nodeFilter = newValue} + } + /// Returns true if `nodeFilter` has been explicitly set. + public var hasNodeFilter: Bool {return _storage._nodeFilter != nil} + /// Clears the value of `nodeFilter`. Subsequent reads from it will return its default value. + public mutating func clearNodeFilter() {_uniqueStorage()._nodeFilter = nil} + + /// + /// Node list highlightening + public var nodeHighlight: NodeHighlight { + get {return _storage._nodeHighlight ?? NodeHighlight()} + set {_uniqueStorage()._nodeHighlight = newValue} + } + /// Returns true if `nodeHighlight` has been explicitly set. + public var hasNodeHighlight: Bool {return _storage._nodeHighlight != nil} + /// Clears the value of `nodeHighlight`. Subsequent reads from it will return its default value. + public mutating func clearNodeHighlight() {_uniqueStorage()._nodeHighlight = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +public struct NodeFilter { + // 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. + + /// + /// Filter unknown nodes + public var unknownSwitch: Bool = false + + /// + /// Filter offline nodes + public var offlineSwitch: Bool = false + + /// + /// Filter nodes w/o public key + public var publicKeySwitch: Bool = false + + /// + /// Filter based on hops away + public var hopsAway: Int32 = 0 + + /// + /// Filter nodes w/o position + public var positionSwitch: Bool = false + + /// + /// Filter nodes by matching name string + public var nodeName: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct NodeHighlight { + // 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. + + /// + /// Hightlight nodes w/ active chat + public var chatSwitch: Bool = false + + /// + /// Highlight nodes w/ position + public var positionSwitch: Bool = false + + /// + /// Highlight nodes w/ telemetry data + public var telemetrySwitch: Bool = false + + /// + /// Highlight nodes w/ iaq data + public var iaqSwitch: Bool = false + + /// + /// Highlight nodes by matching name string + public var nodeName: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Theme: @unchecked Sendable {} +extension Language: @unchecked Sendable {} +extension DeviceUIConfig: @unchecked Sendable {} +extension NodeFilter: @unchecked Sendable {} +extension NodeHighlight: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "meshtastic" + +extension Theme: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DARK"), + 1: .same(proto: "LIGHT"), + 2: .same(proto: "RED"), + ] +} + +extension Language: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "ENGLISH"), + 1: .same(proto: "FRENCH"), + 2: .same(proto: "GERMAN"), + 3: .same(proto: "ITALIAN"), + 4: .same(proto: "PORTUGUESE"), + 5: .same(proto: "SPANISH"), + 6: .same(proto: "SWEDISH"), + 7: .same(proto: "FINNISH"), + 8: .same(proto: "POLISH"), + 9: .same(proto: "TURKISH"), + 10: .same(proto: "SERBIAN"), + 11: .same(proto: "RUSSIAN"), + 12: .same(proto: "DUTCH"), + 13: .same(proto: "GREEK"), + 30: .same(proto: "SIMPLIFIED_CHINESE"), + 31: .same(proto: "TRADITIONAL_CHINESE"), + ] +} + +extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".DeviceUIConfig" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "version"), + 2: .standard(proto: "screen_brightness"), + 3: .standard(proto: "screen_timeout"), + 4: .standard(proto: "screen_lock"), + 5: .standard(proto: "settings_lock"), + 6: .standard(proto: "pin_code"), + 7: .same(proto: "theme"), + 8: .standard(proto: "alert_enabled"), + 9: .standard(proto: "banner_enabled"), + 10: .standard(proto: "ring_tone_id"), + 11: .same(proto: "language"), + 12: .standard(proto: "node_filter"), + 13: .standard(proto: "node_highlight"), + ] + + fileprivate class _StorageClass { + var _version: UInt32 = 0 + var _screenBrightness: UInt32 = 0 + var _screenTimeout: UInt32 = 0 + var _screenLock: Bool = false + var _settingsLock: Bool = false + var _pinCode: UInt32 = 0 + var _theme: Theme = .dark + var _alertEnabled: Bool = false + var _bannerEnabled: Bool = false + var _ringToneID: UInt32 = 0 + var _language: Language = .english + var _nodeFilter: NodeFilter? = nil + var _nodeHighlight: NodeHighlight? = nil + + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif + + private init() {} + + init(copying source: _StorageClass) { + _version = source._version + _screenBrightness = source._screenBrightness + _screenTimeout = source._screenTimeout + _screenLock = source._screenLock + _settingsLock = source._settingsLock + _pinCode = source._pinCode + _theme = source._theme + _alertEnabled = source._alertEnabled + _bannerEnabled = source._bannerEnabled + _ringToneID = source._ringToneID + _language = source._language + _nodeFilter = source._nodeFilter + _nodeHighlight = source._nodeHighlight + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + public mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + 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: &_storage._version) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &_storage._screenBrightness) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &_storage._screenTimeout) }() + case 4: try { try decoder.decodeSingularBoolField(value: &_storage._screenLock) }() + case 5: try { try decoder.decodeSingularBoolField(value: &_storage._settingsLock) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &_storage._pinCode) }() + case 7: try { try decoder.decodeSingularEnumField(value: &_storage._theme) }() + case 8: try { try decoder.decodeSingularBoolField(value: &_storage._alertEnabled) }() + case 9: try { try decoder.decodeSingularBoolField(value: &_storage._bannerEnabled) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &_storage._ringToneID) }() + case 11: try { try decoder.decodeSingularEnumField(value: &_storage._language) }() + case 12: try { try decoder.decodeSingularMessageField(value: &_storage._nodeFilter) }() + case 13: try { try decoder.decodeSingularMessageField(value: &_storage._nodeHighlight) }() + default: break + } + } + } + } + + public func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // 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 _storage._version != 0 { + try visitor.visitSingularUInt32Field(value: _storage._version, fieldNumber: 1) + } + if _storage._screenBrightness != 0 { + try visitor.visitSingularUInt32Field(value: _storage._screenBrightness, fieldNumber: 2) + } + if _storage._screenTimeout != 0 { + try visitor.visitSingularUInt32Field(value: _storage._screenTimeout, fieldNumber: 3) + } + if _storage._screenLock != false { + try visitor.visitSingularBoolField(value: _storage._screenLock, fieldNumber: 4) + } + if _storage._settingsLock != false { + try visitor.visitSingularBoolField(value: _storage._settingsLock, fieldNumber: 5) + } + if _storage._pinCode != 0 { + try visitor.visitSingularUInt32Field(value: _storage._pinCode, fieldNumber: 6) + } + if _storage._theme != .dark { + try visitor.visitSingularEnumField(value: _storage._theme, fieldNumber: 7) + } + if _storage._alertEnabled != false { + try visitor.visitSingularBoolField(value: _storage._alertEnabled, fieldNumber: 8) + } + if _storage._bannerEnabled != false { + try visitor.visitSingularBoolField(value: _storage._bannerEnabled, fieldNumber: 9) + } + if _storage._ringToneID != 0 { + try visitor.visitSingularUInt32Field(value: _storage._ringToneID, fieldNumber: 10) + } + if _storage._language != .english { + try visitor.visitSingularEnumField(value: _storage._language, fieldNumber: 11) + } + try { if let v = _storage._nodeFilter { + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + } }() + try { if let v = _storage._nodeHighlight { + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + } }() + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: DeviceUIConfig, rhs: DeviceUIConfig) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._version != rhs_storage._version {return false} + if _storage._screenBrightness != rhs_storage._screenBrightness {return false} + if _storage._screenTimeout != rhs_storage._screenTimeout {return false} + if _storage._screenLock != rhs_storage._screenLock {return false} + if _storage._settingsLock != rhs_storage._settingsLock {return false} + if _storage._pinCode != rhs_storage._pinCode {return false} + if _storage._theme != rhs_storage._theme {return false} + if _storage._alertEnabled != rhs_storage._alertEnabled {return false} + if _storage._bannerEnabled != rhs_storage._bannerEnabled {return false} + if _storage._ringToneID != rhs_storage._ringToneID {return false} + if _storage._language != rhs_storage._language {return false} + if _storage._nodeFilter != rhs_storage._nodeFilter {return false} + if _storage._nodeHighlight != rhs_storage._nodeHighlight {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension NodeFilter: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".NodeFilter" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "unknown_switch"), + 2: .standard(proto: "offline_switch"), + 3: .standard(proto: "public_key_switch"), + 4: .standard(proto: "hops_away"), + 5: .standard(proto: "position_switch"), + 6: .standard(proto: "node_name"), + ] + + 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.decodeSingularBoolField(value: &self.unknownSwitch) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.offlineSwitch) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.publicKeySwitch) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.hopsAway) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.positionSwitch) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.nodeName) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.unknownSwitch != false { + try visitor.visitSingularBoolField(value: self.unknownSwitch, fieldNumber: 1) + } + if self.offlineSwitch != false { + try visitor.visitSingularBoolField(value: self.offlineSwitch, fieldNumber: 2) + } + if self.publicKeySwitch != false { + try visitor.visitSingularBoolField(value: self.publicKeySwitch, fieldNumber: 3) + } + if self.hopsAway != 0 { + try visitor.visitSingularInt32Field(value: self.hopsAway, fieldNumber: 4) + } + if self.positionSwitch != false { + try visitor.visitSingularBoolField(value: self.positionSwitch, fieldNumber: 5) + } + if !self.nodeName.isEmpty { + try visitor.visitSingularStringField(value: self.nodeName, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: NodeFilter, rhs: NodeFilter) -> Bool { + if lhs.unknownSwitch != rhs.unknownSwitch {return false} + if lhs.offlineSwitch != rhs.offlineSwitch {return false} + if lhs.publicKeySwitch != rhs.publicKeySwitch {return false} + if lhs.hopsAway != rhs.hopsAway {return false} + if lhs.positionSwitch != rhs.positionSwitch {return false} + if lhs.nodeName != rhs.nodeName {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension NodeHighlight: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".NodeHighlight" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "chat_switch"), + 2: .standard(proto: "position_switch"), + 3: .standard(proto: "telemetry_switch"), + 4: .standard(proto: "iaq_switch"), + 5: .standard(proto: "node_name"), + ] + + 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.decodeSingularBoolField(value: &self.chatSwitch) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.positionSwitch) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.telemetrySwitch) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.iaqSwitch) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.nodeName) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.chatSwitch != false { + try visitor.visitSingularBoolField(value: self.chatSwitch, fieldNumber: 1) + } + if self.positionSwitch != false { + try visitor.visitSingularBoolField(value: self.positionSwitch, fieldNumber: 2) + } + if self.telemetrySwitch != false { + try visitor.visitSingularBoolField(value: self.telemetrySwitch, fieldNumber: 3) + } + if self.iaqSwitch != false { + try visitor.visitSingularBoolField(value: self.iaqSwitch, fieldNumber: 4) + } + if !self.nodeName.isEmpty { + try visitor.visitSingularStringField(value: self.nodeName, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: NodeHighlight, rhs: NodeHighlight) -> Bool { + if lhs.chatSwitch != rhs.chatSwitch {return false} + if lhs.positionSwitch != rhs.positionSwitch {return false} + if lhs.telemetrySwitch != rhs.telemetrySwitch {return false} + if lhs.iaqSwitch != rhs.iaqSwitch {return false} + if lhs.nodeName != rhs.nodeName {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 3dd965f2..3349c2c9 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -20,61 +20,6 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -/// -/// Font sizes for the device screen -public enum ScreenFonts: SwiftProtobuf.Enum { - public typealias RawValue = Int - - /// - /// TODO: REPLACE - case fontSmall // = 0 - - /// - /// TODO: REPLACE - case fontMedium // = 1 - - /// - /// TODO: REPLACE - case fontLarge // = 2 - case UNRECOGNIZED(Int) - - public init() { - self = .fontSmall - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .fontSmall - case 1: self = .fontMedium - case 2: self = .fontLarge - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .fontSmall: return 0 - case .fontMedium: return 1 - case .fontLarge: return 2 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension ScreenFonts: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ScreenFonts] = [ - .fontSmall, - .fontMedium, - .fontLarge, - ] -} - -#endif // swift(>=4.2) - /// /// Position with static location information only for NodeDBLite public struct PositionLite { @@ -233,7 +178,7 @@ public struct NodeInfoLite { } /// - /// Number of hops away from us this node is (0 if adjacent) + /// Number of hops away from us this node is (0 if direct neighbor) public var hopsAway: UInt32 { get {return _storage._hopsAway ?? 0} set {_uniqueStorage()._hopsAway = newValue} @@ -392,90 +337,18 @@ public struct ChannelFile { public init() {} } -/// -/// This can be used for customizing the firmware distribution. If populated, -/// show a secondary bootup screen with custom logo and text for 2.5 seconds. -public struct OEMStore { - // 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. - - /// - /// The Logo width in Px - public var oemIconWidth: UInt32 = 0 - - /// - /// The Logo height in Px - public var oemIconHeight: UInt32 = 0 - - /// - /// The Logo in XBM bytechar format - public var oemIconBits: Data = Data() - - /// - /// Use this font for the OEM text. - public var oemFont: ScreenFonts = .fontSmall - - /// - /// Use this font for the OEM text. - public var oemText: String = String() - - /// - /// The default device encryption key, 16 or 32 byte - public var oemAesKey: Data = Data() - - /// - /// A Preset LocalConfig to apply during factory reset - public var oemLocalConfig: LocalConfig { - get {return _oemLocalConfig ?? LocalConfig()} - set {_oemLocalConfig = newValue} - } - /// Returns true if `oemLocalConfig` has been explicitly set. - public var hasOemLocalConfig: Bool {return self._oemLocalConfig != nil} - /// Clears the value of `oemLocalConfig`. Subsequent reads from it will return its default value. - public mutating func clearOemLocalConfig() {self._oemLocalConfig = nil} - - /// - /// A Preset LocalModuleConfig to apply during factory reset - public var oemLocalModuleConfig: LocalModuleConfig { - get {return _oemLocalModuleConfig ?? LocalModuleConfig()} - set {_oemLocalModuleConfig = newValue} - } - /// Returns true if `oemLocalModuleConfig` has been explicitly set. - public var hasOemLocalModuleConfig: Bool {return self._oemLocalModuleConfig != nil} - /// Clears the value of `oemLocalModuleConfig`. Subsequent reads from it will return its default value. - public mutating func clearOemLocalModuleConfig() {self._oemLocalModuleConfig = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _oemLocalConfig: LocalConfig? = nil - fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil -} - #if swift(>=5.5) && canImport(_Concurrency) -extension ScreenFonts: @unchecked Sendable {} extension PositionLite: @unchecked Sendable {} extension UserLite: @unchecked Sendable {} extension NodeInfoLite: @unchecked Sendable {} extension DeviceState: @unchecked Sendable {} extension ChannelFile: @unchecked Sendable {} -extension OEMStore: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" -extension ScreenFonts: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "FONT_SMALL"), - 1: .same(proto: "FONT_MEDIUM"), - 2: .same(proto: "FONT_LARGE"), - ] -} - extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".PositionLite" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -933,81 +806,3 @@ extension ChannelFile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati return true } } - -extension OEMStore: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".OEMStore" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "oem_icon_width"), - 2: .standard(proto: "oem_icon_height"), - 3: .standard(proto: "oem_icon_bits"), - 4: .standard(proto: "oem_font"), - 5: .standard(proto: "oem_text"), - 6: .standard(proto: "oem_aes_key"), - 7: .standard(proto: "oem_local_config"), - 8: .standard(proto: "oem_local_module_config"), - ] - - 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.oemIconWidth) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.oemIconHeight) }() - case 3: try { try decoder.decodeSingularBytesField(value: &self.oemIconBits) }() - case 4: try { try decoder.decodeSingularEnumField(value: &self.oemFont) }() - case 5: try { try decoder.decodeSingularStringField(value: &self.oemText) }() - case 6: try { try decoder.decodeSingularBytesField(value: &self.oemAesKey) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._oemLocalConfig) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._oemLocalModuleConfig) }() - 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.oemIconWidth != 0 { - try visitor.visitSingularUInt32Field(value: self.oemIconWidth, fieldNumber: 1) - } - if self.oemIconHeight != 0 { - try visitor.visitSingularUInt32Field(value: self.oemIconHeight, fieldNumber: 2) - } - if !self.oemIconBits.isEmpty { - try visitor.visitSingularBytesField(value: self.oemIconBits, fieldNumber: 3) - } - if self.oemFont != .fontSmall { - try visitor.visitSingularEnumField(value: self.oemFont, fieldNumber: 4) - } - if !self.oemText.isEmpty { - try visitor.visitSingularStringField(value: self.oemText, fieldNumber: 5) - } - if !self.oemAesKey.isEmpty { - try visitor.visitSingularBytesField(value: self.oemAesKey, fieldNumber: 6) - } - try { if let v = self._oemLocalConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try { if let v = self._oemLocalModuleConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: OEMStore, rhs: OEMStore) -> Bool { - if lhs.oemIconWidth != rhs.oemIconWidth {return false} - if lhs.oemIconHeight != rhs.oemIconHeight {return false} - if lhs.oemIconBits != rhs.oemIconBits {return false} - if lhs.oemFont != rhs.oemFont {return false} - if lhs.oemText != rhs.oemText {return false} - if lhs.oemAesKey != rhs.oemAesKey {return false} - if lhs._oemLocalConfig != rhs._oemLocalConfig {return false} - if lhs._oemLocalModuleConfig != rhs._oemLocalModuleConfig {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index f604d4a7..154d8a6b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -862,7 +862,141 @@ extension CriticalErrorCode: CaseIterable { #endif // swift(>=4.2) /// -/// a gps position +/// Enum for modules excluded from a device's configuration. +/// Each value represents a ModuleConfigType that can be toggled as excluded +/// by setting its corresponding bit in the `excluded_modules` bitmask field. +public enum ExcludedModules: SwiftProtobuf.Enum { + public typealias RawValue = Int + + /// + /// Default value of 0 indicates no modules are excluded. + case excludedNone // = 0 + + /// + /// MQTT module + case mqttConfig // = 1 + + /// + /// Serial module + case serialConfig // = 2 + + /// + /// External Notification module + case extnotifConfig // = 4 + + /// + /// Store and Forward module + case storeforwardConfig // = 8 + + /// + /// Range Test module + case rangetestConfig // = 16 + + /// + /// Telemetry module + case telemetryConfig // = 32 + + /// + /// Canned Message module + case cannedmsgConfig // = 64 + + /// + /// Audio module + case audioConfig // = 128 + + /// + /// Remote Hardware module + case remotehardwareConfig // = 256 + + /// + /// Neighbor Info module + case neighborinfoConfig // = 512 + + /// + /// Ambient Lighting module + case ambientlightingConfig // = 1024 + + /// + /// Detection Sensor module + case detectionsensorConfig // = 2048 + + /// + /// Paxcounter module + case paxcounterConfig // = 4096 + case UNRECOGNIZED(Int) + + public init() { + self = .excludedNone + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .excludedNone + case 1: self = .mqttConfig + case 2: self = .serialConfig + case 4: self = .extnotifConfig + case 8: self = .storeforwardConfig + case 16: self = .rangetestConfig + case 32: self = .telemetryConfig + case 64: self = .cannedmsgConfig + case 128: self = .audioConfig + case 256: self = .remotehardwareConfig + case 512: self = .neighborinfoConfig + case 1024: self = .ambientlightingConfig + case 2048: self = .detectionsensorConfig + case 4096: self = .paxcounterConfig + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .excludedNone: return 0 + case .mqttConfig: return 1 + case .serialConfig: return 2 + case .extnotifConfig: return 4 + case .storeforwardConfig: return 8 + case .rangetestConfig: return 16 + case .telemetryConfig: return 32 + case .cannedmsgConfig: return 64 + case .audioConfig: return 128 + case .remotehardwareConfig: return 256 + case .neighborinfoConfig: return 512 + case .ambientlightingConfig: return 1024 + case .detectionsensorConfig: return 2048 + case .paxcounterConfig: return 4096 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension ExcludedModules: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ExcludedModules] = [ + .excludedNone, + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] +} + +#endif // swift(>=4.2) + +/// +/// A GPS Position public struct Position { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -1846,7 +1980,7 @@ public struct MeshPacket { } /// - /// If unset treated as zero (no forwarding, send to adjacent nodes only) + /// If unset treated as zero (no forwarding, send to direct neighbor nodes only) /// if 1, allow hopping through one node, etc... /// For our usecase real world topologies probably have a max of about 3. /// This field is normally placed into a few of bits in the header. @@ -2218,7 +2352,7 @@ public struct NodeInfo { } /// - /// Number of hops away from us this node is (0 if adjacent) + /// Number of hops away from us this node is (0 if direct neighbor) public var hopsAway: UInt32 { get {return _storage._hopsAway ?? 0} set {_uniqueStorage()._hopsAway = newValue} @@ -2267,6 +2401,14 @@ public struct MyNodeInfo { /// Phone/PC apps should compare this to their build number and if too low tell the user they must update their app public var minAppVersion: UInt32 = 0 + /// + /// Unique hardware identifier for this device + public var deviceID: Data = Data() + + /// + /// The PlatformIO environment used to build this firmware + public var pioEnv: String = String() + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -2586,6 +2728,16 @@ public struct FromRadio { set {payloadVariant = .clientNotification(newValue)} } + /// + /// Persistent data for device-ui + public var deviceuiConfig: DeviceUIConfig { + get { + if case .deviceuiConfig(let v)? = payloadVariant {return v} + return DeviceUIConfig() + } + set {payloadVariant = .deviceuiConfig(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -2644,6 +2796,9 @@ public struct FromRadio { /// /// Notification message to the client case clientNotification(ClientNotification) + /// + /// Persistent data for device-ui + case deviceuiConfig(DeviceUIConfig) #if !swift(>=4.1) public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { @@ -2711,6 +2866,10 @@ public struct FromRadio { guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } return l == r }() + case (.deviceuiConfig, .deviceuiConfig): return { + guard case .deviceuiConfig(let l) = lhs, case .deviceuiConfig(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -3058,6 +3217,11 @@ public struct DeviceMetadata { /// Has PKC capabilities public var hasPkc_p: Bool = false + /// + /// Bit field of boolean for excluded modules + /// (bitwise OR of ExcludedModules) + public var excludedModules: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -3232,6 +3396,7 @@ public struct ChunkedPayloadResponse { extension HardwareModel: @unchecked Sendable {} extension Constants: @unchecked Sendable {} extension CriticalErrorCode: @unchecked Sendable {} +extension ExcludedModules: @unchecked Sendable {} extension Position: @unchecked Sendable {} extension Position.LocSource: @unchecked Sendable {} extension Position.AltSource: @unchecked Sendable {} @@ -3391,6 +3556,25 @@ extension CriticalErrorCode: SwiftProtobuf._ProtoNameProviding { ] } +extension ExcludedModules: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "EXCLUDED_NONE"), + 1: .same(proto: "MQTT_CONFIG"), + 2: .same(proto: "SERIAL_CONFIG"), + 4: .same(proto: "EXTNOTIF_CONFIG"), + 8: .same(proto: "STOREFORWARD_CONFIG"), + 16: .same(proto: "RANGETEST_CONFIG"), + 32: .same(proto: "TELEMETRY_CONFIG"), + 64: .same(proto: "CANNEDMSG_CONFIG"), + 128: .same(proto: "AUDIO_CONFIG"), + 256: .same(proto: "REMOTEHARDWARE_CONFIG"), + 512: .same(proto: "NEIGHBORINFO_CONFIG"), + 1024: .same(proto: "AMBIENTLIGHTING_CONFIG"), + 2048: .same(proto: "DETECTIONSENSOR_CONFIG"), + 4096: .same(proto: "PAXCOUNTER_CONFIG"), + ] +} + extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Position" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -4525,6 +4709,8 @@ extension MyNodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 1: .standard(proto: "my_node_num"), 8: .standard(proto: "reboot_count"), 11: .standard(proto: "min_app_version"), + 12: .standard(proto: "device_id"), + 13: .standard(proto: "pio_env"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4536,6 +4722,8 @@ extension MyNodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 1: try { try decoder.decodeSingularUInt32Field(value: &self.myNodeNum) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.rebootCount) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self.minAppVersion) }() + case 12: try { try decoder.decodeSingularBytesField(value: &self.deviceID) }() + case 13: try { try decoder.decodeSingularStringField(value: &self.pioEnv) }() default: break } } @@ -4551,6 +4739,12 @@ extension MyNodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.minAppVersion != 0 { try visitor.visitSingularUInt32Field(value: self.minAppVersion, fieldNumber: 11) } + if !self.deviceID.isEmpty { + try visitor.visitSingularBytesField(value: self.deviceID, fieldNumber: 12) + } + if !self.pioEnv.isEmpty { + try visitor.visitSingularStringField(value: self.pioEnv, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -4558,6 +4752,8 @@ extension MyNodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.myNodeNum != rhs.myNodeNum {return false} if lhs.rebootCount != rhs.rebootCount {return false} if lhs.minAppVersion != rhs.minAppVersion {return false} + if lhs.deviceID != rhs.deviceID {return false} + if lhs.pioEnv != rhs.pioEnv {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4694,6 +4890,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 14: .same(proto: "mqttClientProxyMessage"), 15: .same(proto: "fileInfo"), 16: .same(proto: "clientNotification"), + 17: .same(proto: "deviceuiConfig"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4888,6 +5085,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.payloadVariant = .clientNotification(v) } }() + case 17: try { + var v: DeviceUIConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .deviceuiConfig(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .deviceuiConfig(v) + } + }() default: break } } @@ -4962,6 +5172,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .clientNotification(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 16) }() + case .deviceuiConfig?: try { + guard case .deviceuiConfig(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 17) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -5351,6 +5565,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 9: .standard(proto: "hw_model"), 10: .same(proto: "hasRemoteHardware"), 11: .same(proto: "hasPKC"), + 12: .standard(proto: "excluded_modules"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -5370,6 +5585,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.hasRemoteHardware_p) }() case 11: try { try decoder.decodeSingularBoolField(value: &self.hasPkc_p) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.excludedModules) }() default: break } } @@ -5409,6 +5625,9 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.hasPkc_p != false { try visitor.visitSingularBoolField(value: self.hasPkc_p, fieldNumber: 11) } + if self.excludedModules != 0 { + try visitor.visitSingularUInt32Field(value: self.excludedModules, fieldNumber: 12) + } try unknownFields.traverse(visitor: &visitor) } @@ -5424,6 +5643,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if lhs.hwModel != rhs.hwModel {return false} if lhs.hasRemoteHardware_p != rhs.hasRemoteHardware_p {return false} if lhs.hasPkc_p != rhs.hasPkc_p {return false} + if lhs.excludedModules != rhs.excludedModules {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 30a8e0a4..4238811f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -455,9 +455,14 @@ public struct ModuleConfig { /// /// Interval in seconds of how often we should try to send our - /// Neighbor Info to the mesh + /// Neighbor Info (minimum is 14400, i.e., 4 hours) public var updateInterval: UInt32 = 0 + /// + /// Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. + /// Note that this is not available on a channel with default key and name. + public var transmitOverLora: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1825,6 +1830,7 @@ extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf. public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "enabled"), 2: .standard(proto: "update_interval"), + 3: .standard(proto: "transmit_over_lora"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1835,6 +1841,7 @@ extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf. switch fieldNumber { case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }() case 2: try { try decoder.decodeSingularUInt32Field(value: &self.updateInterval) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.transmitOverLora) }() default: break } } @@ -1847,12 +1854,16 @@ extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf. if self.updateInterval != 0 { try visitor.visitSingularUInt32Field(value: self.updateInterval, fieldNumber: 2) } + if self.transmitOverLora != false { + try visitor.visitSingularBoolField(value: self.transmitOverLora, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: ModuleConfig.NeighborInfoConfig, rhs: ModuleConfig.NeighborInfoConfig) -> Bool { if lhs.enabled != rhs.enabled {return false} if lhs.updateInterval != rhs.updateInterval {return false} + if lhs.transmitOverLora != rhs.transmitOverLora {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index ec5faaa4..e67b5272 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -150,8 +150,12 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case max30102 // = 30 /// - /// MLX90614 non-contact IR temperature sensor. + /// MLX90614 non-contact IR temperature sensor case mlx90614 // = 31 + + /// + /// SCD40/SCD41 CO2, humidity, temperature sensor + case scd4X // = 32 case UNRECOGNIZED(Int) public init() { @@ -192,6 +196,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 29: self = .customSensor case 30: self = .max30102 case 31: self = .mlx90614 + case 32: self = .scd4X default: self = .UNRECOGNIZED(rawValue) } } @@ -230,6 +235,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .customSensor: return 29 case .max30102: return 30 case .mlx90614: return 31 + case .scd4X: return 32 case .UNRECOGNIZED(let i): return i } } @@ -273,6 +279,7 @@ extension TelemetrySensorType: CaseIterable { .customSensor, .max30102, .mlx90614, + .scd4X, ] } @@ -778,6 +785,17 @@ public struct AirQualityMetrics { /// Clears the value of `particles100Um`. Subsequent reads from it will return its default value. public mutating func clearParticles100Um() {self._particles100Um = nil} + /// + /// 10.0um Particle Count + public var co2: UInt32 { + get {return _co2 ?? 0} + set {_co2 = newValue} + } + /// Returns true if `co2` has been explicitly set. + public var hasCo2: Bool {return self._co2 != nil} + /// Clears the value of `co2`. Subsequent reads from it will return its default value. + public mutating func clearCo2() {self._co2 = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -794,6 +812,7 @@ public struct AirQualityMetrics { fileprivate var _particles25Um: UInt32? = nil fileprivate var _particles50Um: UInt32? = nil fileprivate var _particles100Um: UInt32? = nil + fileprivate var _co2: UInt32? = nil } /// @@ -1108,6 +1127,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 29: .same(proto: "CUSTOM_SENSOR"), 30: .same(proto: "MAX30102"), 31: .same(proto: "MLX90614"), + 32: .same(proto: "SCD4X"), ] } @@ -1456,6 +1476,7 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 10: .standard(proto: "particles_25um"), 11: .standard(proto: "particles_50um"), 12: .standard(proto: "particles_100um"), + 13: .same(proto: "co2"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1476,6 +1497,7 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem case 10: try { try decoder.decodeSingularUInt32Field(value: &self._particles25Um) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self._particles50Um) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self._particles100Um) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self._co2) }() default: break } } @@ -1522,6 +1544,9 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem try { if let v = self._particles100Um { try visitor.visitSingularUInt32Field(value: v, fieldNumber: 12) } }() + try { if let v = self._co2 { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 13) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -1538,6 +1563,7 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if lhs._particles25Um != rhs._particles25Um {return false} if lhs._particles50Um != rhs._particles50Um {return false} if lhs._particles100Um != rhs._particles100Um {return false} + if lhs._co2 != rhs._co2 {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/protobufs b/protobufs index c9ae7fd4..04f21f5c 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c9ae7fd478bffe5f954b30de6cb140821fe9ff52 +Subproject commit 04f21f5c7238b8e02f794d9282c4786752634b3c From 696d197f40d2f70bb52fd5ab96c4d371cb987b59 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 12 Nov 2024 08:27:57 -0800 Subject: [PATCH 293/333] Update device hardware json --- Meshtastic/Resources/DeviceHardware.json | 372 +++++++++++++++++++---- 1 file changed, 321 insertions(+), 51 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 0b88202d..5e0135b9 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -5,7 +5,10 @@ "platformioTarget": "tlora-v2", "architecture": "esp32", "activelySupported": false, - "displayName": "LILYGO T-LoRa V2" + "displayName": "LILYGO T-LoRa V2", + "tags": [ + "LilyGo" + ] }, { "hwModel": 2, @@ -13,7 +16,10 @@ "platformioTarget": "tlora-v1", "architecture": "esp32", "activelySupported": false, - "displayName": "LILYGO T-LoRa V1" + "displayName": "LILYGO T-LoRa V1", + "tags": [ + "LilyGo" + ] }, { "hwModel": 3, @@ -21,7 +27,14 @@ "platformioTarget": "tlora-v2-1-1_6", "architecture": "esp32", "activelySupported": true, - "displayName": "LILYGO T-LoRa V2.1-1.6" + "supportLevel": 1, + "displayName": "LILYGO T-LoRa V2.1-1.6", + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-v2-1-1_6.svg" + ] }, { "hwModel": 4, @@ -29,7 +42,14 @@ "platformioTarget": "tbeam", "architecture": "esp32", "activelySupported": true, - "displayName": "LILYGO T-Beam" + "supportLevel": 1, + "displayName": "LILYGO T-Beam", + "tags": [ + "LilyGo" + ], + "images": [ + "tbeam.svg" + ] }, { "hwModel": 5, @@ -37,7 +57,10 @@ "platformioTarget": "heltec-v2_0", "architecture": "esp32", "activelySupported": false, - "displayName": "Heltec V2.0" + "displayName": "Heltec V2.0", + "tags": [ + "Heltec" + ] }, { "hwModel": 6, @@ -45,15 +68,25 @@ "platformioTarget": "tbeam0_7", "architecture": "esp32", "activelySupported": false, - "displayName": "LILYGO T-Beam V0.7" + "displayName": "LILYGO T-Beam V0.7", + "tags": [ + "LilyGo" + ] }, { "hwModel": 7, "hwModelSlug": "T_ECHO", "platformioTarget": "t-echo", "architecture": "nrf52840", + "supportLevel": 1, "activelySupported": true, - "displayName": "LILYGO T-Echo" + "displayName": "LILYGO T-Echo", + "tags": [ + "LilyGo" + ], + "images": [ + "t-echo.svg" + ] }, { "hwModel": 8, @@ -61,7 +94,10 @@ "platformioTarget": "tlora-v1_3", "architecture": "esp32", "activelySupported": false, - "displayName": "LILYGO T-LoRa V1.1-1.3" + "displayName": "LILYGO T-LoRa V1.1-1.3", + "tags": [ + "LilyGo" + ] }, { "hwModel": 9, @@ -69,7 +105,15 @@ "platformioTarget": "rak4631", "architecture": "nrf52840", "activelySupported": true, - "displayName": "RAK WisBlock 4631" + "supportLevel": 1, + "displayName": "RAK WisBlock 4631", + "tags": [ + "RAK" + ], + "images": [ + "rak4631.svg", + "rak4631_case.svg" + ] }, { "hwModel": 10, @@ -77,7 +121,10 @@ "platformioTarget": "heltec-v2_1", "architecture": "esp32", "activelySupported": false, - "displayName": "Heltec V2.1" + "displayName": "Heltec V2.1", + "tags": [ + "Heltec" + ] }, { "hwModel": 11, @@ -85,7 +132,10 @@ "platformioTarget": "heltec-v1", "architecture": "esp32", "activelySupported": false, - "displayName": "Heltec V1" + "displayName": "Heltec V1", + "tags": [ + "Heltec" + ] }, { "hwModel": 12, @@ -93,7 +143,14 @@ "platformioTarget": "tbeam-s3-core", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "LILYGO T-Beam S3 Core" + "supportLevel": 1, + "displayName": "LILYGO T-Beam Supreme", + "tags": [ + "LilyGo" + ], + "images": [ + "tbeam-s3-core.svg" + ] }, { "hwModel": 13, @@ -101,7 +158,10 @@ "platformioTarget": "rak11200", "architecture": "esp32", "activelySupported": false, - "displayName": "RAK WisBlock 11200" + "displayName": "RAK WisBlock 11200", + "tags": [ + "RAK" + ] }, { "hwModel": 14, @@ -109,7 +169,11 @@ "platformioTarget": "nano-g1", "architecture": "esp32", "activelySupported": true, - "displayName": "Nano G1" + "supportLevel": 3, + "displayName": "Nano G1", + "tags": [ + "B&Q" + ] }, { "hwModel": 15, @@ -117,7 +181,15 @@ "platformioTarget": "tlora-v2-1-1_8", "architecture": "esp32", "activelySupported": true, - "displayName": "LILYGO T-LoRa V2.1-1.8" + "supportLevel": 2, + "displayName": "LILYGO T-LoRa V2.1-1.8", + "tags": [ + "LilyGo", + "2.4G LoRA" + ], + "images": [ + "tlora-v2-1-1_8.svg" + ] }, { "hwModel": 16, @@ -125,7 +197,14 @@ "platformioTarget": "tlora-t3s3-v1", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "LILYGO T-LoRa T3-S3" + "displayName": "LILYGO T-LoRa T3-S3", + "supportLevel": 1, + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-t3s3-v1.svg" + ] }, { "hwModel": 16, @@ -133,7 +212,14 @@ "platformioTarget": "tlora-t3s3-epaper", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "LILYGO T-LoRa T3-S3 E-Paper" + "supportLevel": 1, + "displayName": "LILYGO T-LoRa T3-S3 E-Ink", + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-t3s3-epaper.svg" + ] }, { "hwModel": 17, @@ -141,7 +227,11 @@ "platformioTarget": "nano-g1-explorer", "architecture": "esp32", "activelySupported": true, - "displayName": "Nano G1 Explorer" + "supportLevel": 3, + "displayName": "Nano G1 Explorer", + "tags": [ + "B&Q" + ] }, { "hwModel": 18, @@ -149,7 +239,11 @@ "platformioTarget": "nano-g2-ultra", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Nano G2 Ultra" + "supportLevel": 2, + "displayName": "Nano G2 Ultra", + "tags": [ + "B&Q" + ] }, { "hwModel": 21, @@ -157,7 +251,14 @@ "platformioTarget": "wio-tracker-wm1110", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Seeed Wio WM1110 Tracker" + "supportLevel": 1, + "displayName": "Seeed Wio WM1110 Tracker", + "tags": [ + "Seeed" + ], + "images": [ + "wio-tracker-wm1110.svg" + ] }, { "hwModel": 25, @@ -165,7 +266,11 @@ "platformioTarget": "station-g1", "architecture": "esp32", "activelySupported": true, - "displayName": "Station G1" + "supportLevel": 3, + "displayName": "Station G1", + "tags": [ + "B&Q" + ] }, { "hwModel": 26, @@ -173,7 +278,11 @@ "platformioTarget": "rak11310", "architecture": "rp2040", "activelySupported": true, - "displayName": "RAK WisBlock 11310" + "supportLevel": 2, + "displayName": "RAK WisBlock 11310", + "tags": [ + "RAK" + ] }, { "hwModel": 29, @@ -181,7 +290,11 @@ "platformioTarget": "canaryone", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Canary One" + "supportLevel": 3, + "displayName": "Canary One", + "tags": [ + "Canary" + ] }, { "hwModel": 30, @@ -189,7 +302,11 @@ "platformioTarget": "rp2040-lora", "architecture": "rp2040", "activelySupported": true, - "displayName": "RP2040 LoRa" + "supportLevel": 2, + "displayName": "RP2040 LoRa", + "tags": [ + "Waveshare" + ] }, { "hwModel": 31, @@ -197,7 +314,11 @@ "platformioTarget": "station-g2", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Station G2" + "supportLevel": 2, + "displayName": "Station G2", + "tags": [ + "B&Q" + ] }, { "hwModel": 39, @@ -205,7 +326,11 @@ "platformioTarget": "meshtastic-diy-v1", "architecture": "esp32", "activelySupported": true, - "displayName": "DIY V1" + "supportLevel": 3, + "displayName": "DIY V1", + "tags": [ + "DIY" + ] }, { "hwModel": 39, @@ -213,15 +338,22 @@ "platformioTarget": "hydra", "architecture": "esp32", "activelySupported": true, - "displayName": "Hydra" + "supportLevel": 3, + "displayName": "Hydra", + "tags": [ + "DIY" + ] }, { "hwModel": 41, "hwModelSlug": "DR_DEV", "platformioTarget": "meshtastic-dr-dev", "architecture": "esp32", - "activelySupported": true, - "displayName": "DR-DEV" + "activelySupported": false, + "displayName": "DR-DEV", + "tags": [ + "DIY" + ] }, { "hwModel": 42, @@ -229,7 +361,11 @@ "platformioTarget": "m5stack-core", "architecture": "esp32", "activelySupported": true, - "displayName": "M5 Stack" + "supportLevel": 3, + "displayName": "M5 Stack", + "tags": [ + "M5Stack" + ] }, { "hwModel": 43, @@ -237,7 +373,15 @@ "platformioTarget": "heltec-v3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec V3" + "supportLevel": 1, + "displayName": "Heltec V3", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-v3.svg", + "heltec-v3-case.svg" + ] }, { "hwModel": 44, @@ -245,7 +389,14 @@ "platformioTarget": "heltec-wsl-v3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Wireless Stick Lite V3" + "supportLevel": 1, + "displayName": "Heltec Wireless Stick Lite V3", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-wsl-v3.svg" + ] }, { "hwModel": 47, @@ -253,7 +404,12 @@ "platformioTarget": "pico", "architecture": "rp2040", "activelySupported": true, - "displayName": "Raspberry Pi Pico" + "supportLevel": 3, + "displayName": "Raspberry Pi Pico", + "tags": [ + "Raspberry Pi", + "DIY" + ] }, { "hwModel": 47, @@ -261,7 +417,12 @@ "platformioTarget": "picow", "architecture": "rp2040", "activelySupported": true, - "displayName": "Raspberry Pi Pico W" + "supportLevel": 3, + "displayName": "Raspberry Pi Pico W", + "tags": [ + "Raspberry Pi", + "DIY" + ] }, { "hwModel": 48, @@ -269,7 +430,14 @@ "platformioTarget": "heltec-wireless-tracker", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Wireless Tracker V1.1" + "supportLevel": 1, + "displayName": "Heltec Wireless Tracker V1.1", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-wireless-tracker.svg" + ] }, { "hwModel": 58, @@ -277,7 +445,11 @@ "platformioTarget": "heltec-wireless-tracker-V1-0", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Wireless Tracker V1.0" + "supportLevel": 3, + "displayName": "Heltec Wireless Tracker V1.0", + "images": [ + "heltec-wireless-tracker.svg" + ] }, { "hwModel": 49, @@ -285,7 +457,14 @@ "platformioTarget": "heltec-wireless-paper", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Wireless Paper" + "supportLevel": 1, + "displayName": "Heltec Wireless Paper", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-wireless-paper.svg" + ] }, { "hwModel": 50, @@ -293,7 +472,14 @@ "platformioTarget": "t-deck", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "LILYGO T-Deck" + "supportLevel": 1, + "displayName": "LILYGO T-Deck", + "tags": [ + "LilyGo" + ], + "images": [ + "t-deck.svg" + ] }, { "hwModel": 51, @@ -301,7 +487,14 @@ "platformioTarget": "t-watch-s3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "LILYGO T-Watch S3" + "supportLevel": 1, + "displayName": "LILYGO T-Watch S3", + "tags": [ + "LilyGo" + ], + "images": [ + "t-watch-s3.svg" + ] }, { "hwModel": 52, @@ -309,6 +502,7 @@ "platformioTarget": "picomputer-s3", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "Pi Computer S3" }, { @@ -316,8 +510,15 @@ "hwModelSlug": "HELTEC_HT62", "platformioTarget": "heltec-ht62-esp32c3-sx1262", "architecture": "esp32-c3", + "supportLevel": 1, "activelySupported": true, - "displayName": "Heltec HT62" + "displayName": "Heltec HT62", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-ht62-esp32c3-sx1262.svg" + ] }, { "hwModel": 57, @@ -325,7 +526,14 @@ "platformioTarget": "heltec-wireless-paper-v1_0", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Wireless Paper V1.0" + "supportLevel": 3, + "tags": [ + "Heltec" + ], + "displayName": "Heltec Wireless Paper V1.0", + "images": [ + "heltec-wireless-paper-v1_0.svg" + ] }, { "hwModel": 59, @@ -333,6 +541,7 @@ "platformioTarget": "unphone", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "unPhone" }, { @@ -341,6 +550,7 @@ "platformioTarget": "tracksenger", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "TrackSenger (small TFT)" }, { @@ -349,6 +559,7 @@ "platformioTarget": "tracksenger-lcd", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "TrackSenger (big TFT)" }, { @@ -357,6 +568,7 @@ "platformioTarget": "tracksenger-oled", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "TrackSenger (big OLED)" }, { @@ -365,7 +577,11 @@ "platformioTarget": "CDEBYTE_EoRa-S3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "EBYTE EoRa-S3" + "supportLevel": 3, + "displayName": "EBYTE EoRa-S3", + "tags": [ + "EByte" + ] }, { "hwModel": 64, @@ -373,7 +589,11 @@ "platformioTarget": "radiomaster_900_bandit_nano", "architecture": "esp32", "activelySupported": true, - "displayName": "RadioMaster 900 Bandit Nano" + "supportLevel": 2, + "displayName": "RadioMaster 900 Bandit Nano", + "tags": [ + "RadioMaster" + ] }, { "hwModel": 66, @@ -381,7 +601,14 @@ "platformioTarget": "heltec-vision-master-t190", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Vision Master T190" + "supportLevel": 1, + "displayName": "Heltec Vision Master T190", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-t190.svg" + ] }, { "hwModel": 67, @@ -389,7 +616,14 @@ "platformioTarget": "heltec-vision-master-e213", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Vision Master E213" + "supportLevel": 1, + "displayName": "Heltec Vision Master E213", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-e213.svg" + ] }, { "hwModel": 68, @@ -397,15 +631,30 @@ "platformioTarget": "heltec-vision-master-e290", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Heltec Vision Master E290" + "supportLevel": 1, + "displayName": "Heltec Vision Master E290", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-e290.svg" + ] }, { "hwModel": 69, "hwModelSlug": "HELTEC_MESH_NODE_T114", "platformioTarget": "heltec-mesh-node-t114", "architecture": "nrf52840", - "activelySupported": false, - "displayName": "Heltec Mesh Node T114" + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Mesh Node T114", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-mesh-node-t114.svg", + "heltec-mesh-node-t114-case.svg" + ] }, { "hwModel": 70, @@ -413,7 +662,14 @@ "platformioTarget": "seeed-sensecap-indicator", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "SenseCAP Indicator" + "supportLevel": 1, + "displayName": "Seeed SenseCAP Indicator", + "tags": [ + "Seeed" + ], + "images": [ + "seeed-sensecap-indicator.svg" + ] }, { "hwModel": 71, @@ -421,7 +677,14 @@ "platformioTarget": "tracker-t1000-e", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Seeed Card Tracker T1000-E" + "supportLevel": 1, + "displayName": "Seeed Card Tracker T1000-E", + "tags": [ + "Seeed" + ], + "images": [ + "tracker-t1000-e.svg" + ] }, { "hwModel": 72, @@ -429,6 +692,13 @@ "platformioTarget": "seeed-xiao-s3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Seeed XIAO S3" + "supportLevel": 1, + "displayName": "Seeed Xiao ESP32-S3", + "tags": [ + "Seeed" + ], + "images": [ + "seeed-xiao-s3.svg" + ] } ] From 2c4cf4cc57844acd6c916f71344d72021c9b1556 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 14 Nov 2024 08:11:49 -0800 Subject: [PATCH 294/333] Admin key fixes --- Meshtastic/Persistence/UpdateCoreData.swift | 5 +++++ Meshtastic/Views/Settings/Config/SecurityConfig.swift | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6ddd664c..9feecf6d 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -831,6 +831,11 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s fetchedNode[0].securityConfig?.privateKey = config.privateKey if config.adminKey.count > 0 { fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] + if config.adminKey.count > 1 { + fetchedNode[0].securityConfig?.adminKey = config.adminKey[1] + } else if config.adminKey.count > 2 { + fetchedNode[0].securityConfig?.adminKey = config.adminKey[2] + } } fetchedNode[0].securityConfig?.isManaged = config.isManaged fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 8ddd5816..206b0de0 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -85,7 +85,7 @@ struct SecurityConfig: View { .font(idiom == .phone ? .caption : .callout) Divider() Label("Tertiary Admin Key", systemImage: "key.viewfinder") - SecureInput("Tertiary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey2) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) @@ -259,8 +259,8 @@ struct SecurityConfig: View { self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? "" self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" - self.adminKey2 = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" - self.adminKey3 = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" + self.adminKey2 = node?.securityConfig?.adminKey2?.base64EncodedString() ?? "" + self.adminKey3 = node?.securityConfig?.adminKey3?.base64EncodedString() ?? "" self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false From 68b6ffa3d6c1aed23dd48162e936cdf409ea5266 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Fri, 1 Nov 2024 12:29:40 -0500 Subject: [PATCH 295/333] Refactor route string buiding --- Meshtastic/Helpers/BLEManager.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 13636774..bb707885 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -890,7 +890,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } hopNodes.append(traceRouteHop) - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr != -32 ? String(traceRouteHop.snr) : "unknown ".localized)dB) --> " + + let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized)) + let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : "" + let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized + routeString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> " } let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized @@ -905,7 +909,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute?.hasPositions = true } hopNodes.append(destinationHop) - /// Add the destination node to the end of the route towards string and the beginning of teh route back string + /// Add the destination node to the end of the route towards string and the beginning of the route back string routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)" traceRoute?.routeText = routeString @@ -942,7 +946,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } hopNodes.append(traceRouteHop) - routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr != -32 ? String(traceRouteHop.snr) : "unknown ".localized)dB) --> " + + let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized)) + let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : "" + let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized + routeBackString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> " } // If nil, set to unknown, INT8_MIN (-128) then divide by 4 let snrBackLast = Float(routingMessage.snrBack.last ?? -128) / 4 From 564fadd0174e1f99ff8fc30864519e9877561e2e Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 2 Nov 2024 11:18:21 -0500 Subject: [PATCH 296/333] Update traceroute list naming --- Localizable.xcstrings | 17 ++++++++++++----- Meshtastic/Views/Nodes/TraceRouteLog.swift | 13 +++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2881b290..15b0fa30 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -38,18 +38,25 @@ } } }, - "%@ - %d Hops Towards %d Hops Back" : { + "%@ - %@ Towards %@ Back" : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$@ - %2$d Hops Towards %3$d Hops Back" + "value" : "%1$@ - %2$@ Towards %3$@ Back" } } } }, - "%@ - 1 Hop" : { - + "%@ - %d %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$d %3$@" + } + } + } }, "%@ - Direct" : { @@ -24065,4 +24072,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index e6e72841..6ffcc58f 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -37,14 +37,19 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - if route.response && route.hopsTowards == 0 { + if route.response && route.hopsTowards == 0 && route.hopsBack == 0 { Text("\(route.time?.formatted() ?? "unknown".localized) - Direct") .font(.caption) - } else if route.response && route.hopsTowards == 1 { - Text("\(route.time?.formatted() ?? "unknown".localized) - 1 Hop") + } else if route.response && route.hopsTowards == route.hopsBack { + let hopLabel = route.hopsTowards == 1 ? "Hop" : "Hops" + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.hopsTowards) \(hopLabel)") .font(.caption) } else if route.response { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.hopsTowards) Hops Towards \(route.hopsBack) Hops Back") + let hopTowardsLabel = route.hopsTowards == 1 ? "Hop" : "Hops" + let hopBackLabel = route.hopsBack == 1 ? "Hop" : "Hops" + let hopTowardsString = (route.hopsTowards == 0) ? "Direct" : "\(route.hopsTowards) \(hopTowardsLabel)" + let hopBackString = (route.hopsBack == 0) ? "Direct" : "\(route.hopsBack) \(hopBackLabel)" + Text("\(route.time?.formatted() ?? "unknown".localized) - \(hopTowardsString) Towards \(hopBackString) Back") .font(.caption) } else if route.sent { Text("\(route.time?.formatted() ?? "unknown".localized) - No Response") From c66eac3f485992bd2d618d52eafdc522bf051f16 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 16 Nov 2024 09:55:03 -0600 Subject: [PATCH 297/333] Allow view to manage sizing to prevent selected node view from getting pushed off bottom of screen --- Meshtastic/Views/Nodes/TraceRouteLog.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index e6e72841..f1095ad5 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -72,7 +72,6 @@ struct TraceRouteLog: View { } .listStyle(.plain) } - .frame(minHeight: CGFloat((node.traceRoutes?.count ?? 0) * 40), maxHeight: 250) Divider() ScrollView { if selectedRoute != nil { From 4b9b701b108f9c05936d6d43b8c74481f09e6a2e Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 16 Nov 2024 13:46:22 -0600 Subject: [PATCH 298/333] Add shorter distance options to distance filtering --- Meshtastic/Enums/AppSettingsEnums.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 690fe399..b12a10a6 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -51,6 +51,10 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable { } enum MeshMapDistances: Double, CaseIterable, Identifiable { + case twoMiles = 3218.69 + case fiveMiles = 8046.72 + case tenMiles = 16093.4 + case twentyFiveMiles = 40233.6 case fiftyMiles = 80467.2 case oneHundredMiles = 160934 case twoHundredMiles = 321869 From b4f67377aaa2f2f4339e2a0f2a7c0ce59c71d13c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 18 Nov 2024 16:11:16 -0800 Subject: [PATCH 299/333] Update bug.yml --- .github/ISSUE_TEMPLATE/bug.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 6adc9c60..872227a2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -52,3 +52,12 @@ body: attributes: label: Additional comments description: Is there anything else that's important for the team to know? + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://meshtastic.org/docs/legal/conduct/). + options: + - label: I agree to follow this project's Code of Conduct + required: true From b0e3960fba6defe50fca4a7f093430a9c1858d72 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 18 Nov 2024 16:11:58 -0800 Subject: [PATCH 300/333] Update feature.yml --- .github/ISSUE_TEMPLATE/feature.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index f0520eef..3913159e 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -39,3 +39,11 @@ body: attributes: label: Additional comments description: Is there anything else that's important for the team to know? + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://meshtastic.org/docs/legal/conduct/). + options: + - label: I agree to follow this project's Code of Conduct + required: true From 579e5be7670613346899fa465d516e6795703156 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Nov 2024 22:15:24 -0800 Subject: [PATCH 301/333] Update stale.yml --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3521f916..a567b9e1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,5 +18,5 @@ jobs: - name: Stale PR+Issues uses: actions/stale@v9.0.0 with: - exempt-issue-labels: help wanted + exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog' exempt-pr-labels: From 385d6ca942929ed09bad9a4802e1e1e29cc0a59f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 23 Nov 2024 22:16:37 -0800 Subject: [PATCH 302/333] Update stale.yml --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a567b9e1..94744d0c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,5 +18,5 @@ jobs: - name: Stale PR+Issues uses: actions/stale@v9.0.0 with: - exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog' - exempt-pr-labels: + exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue' + exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue' From de9bf4d52eb51da3d59e46f99f157a3da27cb116 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 25 Nov 2024 13:12:43 -0800 Subject: [PATCH 303/333] Handle connsecutive capital letters in camelCaseToWords function --- Meshtastic/Extensions/String.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 4e840a98..b001f2eb 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -61,11 +61,11 @@ extension String { } func camelCaseToWords() -> String { - return unicodeScalars.dropFirst().reduce(String(prefix(1))) { - return CharacterSet.uppercaseLetters.contains($1) - ? $0 + " " + String($1) - : $0 + String($1) - } + return self + .replacingOccurrences(of: "([a-z])([A-Z](?=[A-Z])[a-z]*)", with: "$1 $2", options: .regularExpression) + .replacingOccurrences(of: "([A-Z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression) + .replacingOccurrences(of: "([a-z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression) + .replacingOccurrences(of: "([a-z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression) } var length: Int { From d4013f47cba2b6c3d222bb918aaf59d81a37bbf0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Nov 2024 08:05:08 -0800 Subject: [PATCH 304/333] New device hardware file --- Meshtastic/Resources/DeviceHardware.json | 103 +++++++++++++++++------ 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 5e0135b9..94c74660 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -86,7 +86,8 @@ ], "images": [ "t-echo.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 8, @@ -113,7 +114,8 @@ "images": [ "rak4631.svg", "rak4631_case.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 10, @@ -150,7 +152,8 @@ ], "images": [ "tbeam-s3-core.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 13, @@ -204,7 +207,8 @@ ], "images": [ "tlora-t3s3-v1.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 16, @@ -219,7 +223,8 @@ ], "images": [ "tlora-t3s3-epaper.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 17, @@ -243,6 +248,10 @@ "displayName": "Nano G2 Ultra", "tags": [ "B&Q" + ], + "requiresDfu": true, + "images": [ + "nano-g2-ultra.svg" ] }, { @@ -258,7 +267,8 @@ ], "images": [ "wio-tracker-wm1110.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 25, @@ -282,7 +292,11 @@ "displayName": "RAK WisBlock 11310", "tags": [ "RAK" - ] + ], + "images": [ + "rak4631.svg" + ], + "requiresDfu": true }, { "hwModel": 29, @@ -294,7 +308,8 @@ "displayName": "Canary One", "tags": [ "Canary" - ] + ], + "requiresDfu": true }, { "hwModel": 30, @@ -306,7 +321,8 @@ "displayName": "RP2040 LoRa", "tags": [ "Waveshare" - ] + ], + "requiresDfu": true }, { "hwModel": 31, @@ -318,6 +334,10 @@ "displayName": "Station G2", "tags": [ "B&Q" + ], + "requiresDfu": true, + "images": [ + "station-g2.svg" ] }, { @@ -409,7 +429,8 @@ "tags": [ "Raspberry Pi", "DIY" - ] + ], + "requiresDfu": true }, { "hwModel": 47, @@ -422,7 +443,8 @@ "tags": [ "Raspberry Pi", "DIY" - ] + ], + "requiresDfu": true }, { "hwModel": 48, @@ -437,19 +459,21 @@ ], "images": [ "heltec-wireless-tracker.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 58, "hwModelSlug": "HELTEC_WIRELESS_TRACKER_V1_0", "platformioTarget": "heltec-wireless-tracker-V1-0", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 3, "displayName": "Heltec Wireless Tracker V1.0", "images": [ "heltec-wireless-tracker.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 49, @@ -479,7 +503,8 @@ ], "images": [ "t-deck.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 51, @@ -525,7 +550,7 @@ "hwModelSlug": "HELTEC_WIRELESS_PAPER_V1_0", "platformioTarget": "heltec-wireless-paper-v1_0", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 3, "tags": [ "Heltec" @@ -542,7 +567,8 @@ "architecture": "esp32-s3", "activelySupported": true, "supportLevel": 3, - "displayName": "unPhone" + "displayName": "unPhone", + "requiresDfu": true }, { "hwModel": 48, @@ -551,7 +577,8 @@ "architecture": "esp32-s3", "activelySupported": true, "supportLevel": 3, - "displayName": "TrackSenger (small TFT)" + "displayName": "TrackSenger (small TFT)", + "requiresDfu": true }, { "hwModel": 48, @@ -560,7 +587,8 @@ "architecture": "esp32-s3", "activelySupported": true, "supportLevel": 3, - "displayName": "TrackSenger (big TFT)" + "displayName": "TrackSenger (big TFT)", + "requiresDfu": true }, { "hwModel": 48, @@ -581,7 +609,8 @@ "displayName": "EBYTE EoRa-S3", "tags": [ "EByte" - ] + ], + "requiresDfu": true }, { "hwModel": 64, @@ -608,7 +637,8 @@ ], "images": [ "heltec-vision-master-t190.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 67, @@ -623,7 +653,8 @@ ], "images": [ "heltec-vision-master-e213.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 68, @@ -638,7 +669,8 @@ ], "images": [ "heltec-vision-master-e290.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 69, @@ -654,7 +686,8 @@ "images": [ "heltec-mesh-node-t114.svg", "heltec-mesh-node-t114-case.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 70, @@ -684,7 +717,8 @@ ], "images": [ "tracker-t1000-e.svg" - ] + ], + "requiresDfu": true }, { "hwModel": 72, @@ -699,6 +733,23 @@ ], "images": [ "seeed-xiao-s3.svg" - ] + ], + "requiresDfu": true + }, + { + "hwModel": 84, + "hwModelSlug": "WISMESH_TAP", + "platformioTarget": "rak_wismeshtap", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "RAK WisMesh Tap", + "tags": [ + "RAK" + ], + "images": [ + "rak-wismeshtap.svg" + ], + "requiresDfu": true } ] From c3d90f12c608d90f8c1dde5a400580484721adce Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Nov 2024 08:14:27 -0800 Subject: [PATCH 305/333] Update protos --- .../Sources/meshtastic/admin.pb.swift | 60 +++++++++++++++++ .../Sources/meshtastic/device_ui.pb.swift | 23 +++++++ .../Sources/meshtastic/deviceonly.pb.swift | 31 +++++++++ .../Sources/meshtastic/mesh.pb.swift | 65 +++++++++++++++++-- .../Sources/meshtastic/telemetry.pb.swift | 27 ++++++++ protobufs | 2 +- 6 files changed, 203 insertions(+), 5 deletions(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 9e7fe0c5..1f51447d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -416,6 +416,26 @@ public struct AdminMessage { set {payloadVariant = .storeUiConfig(newValue)} } + /// + /// Set specified node-num to be ignored on the NodeDB on the device + public var setIgnoredNode: UInt32 { + get { + if case .setIgnoredNode(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .setIgnoredNode(newValue)} + } + + /// + /// Set specified node-num to be un-ignored on the NodeDB on the device + public var removeIgnoredNode: UInt32 { + get { + if case .removeIgnoredNode(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .removeIgnoredNode(newValue)} + } + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) @@ -633,6 +653,12 @@ public struct AdminMessage { /// Tell the node to store UI data persistently. case storeUiConfig(DeviceUIConfig) /// + /// Set specified node-num to be ignored on the NodeDB on the device + case setIgnoredNode(UInt32) + /// + /// Set specified node-num to be un-ignored on the NodeDB on the device + case removeIgnoredNode(UInt32) + /// /// Begins an edit transaction for config, module config, owner, and channel settings changes /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) case beginEditSettings(Bool) @@ -817,6 +843,14 @@ public struct AdminMessage { guard case .storeUiConfig(let l) = lhs, case .storeUiConfig(let r) = rhs else { preconditionFailure() } return l == r }() + case (.setIgnoredNode, .setIgnoredNode): return { + guard case .setIgnoredNode(let l) = lhs, case .setIgnoredNode(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.removeIgnoredNode, .removeIgnoredNode): return { + guard case .removeIgnoredNode(let l) = lhs, case .removeIgnoredNode(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.beginEditSettings, .beginEditSettings): return { guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } return l == r @@ -1184,6 +1218,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 44: .standard(proto: "get_ui_config_request"), 45: .standard(proto: "get_ui_config_response"), 46: .standard(proto: "store_ui_config"), + 47: .standard(proto: "set_ignored_node"), + 48: .standard(proto: "remove_ignored_node"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), 94: .standard(proto: "factory_reset_device"), @@ -1572,6 +1608,22 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .storeUiConfig(v) } }() + case 47: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .setIgnoredNode(v) + } + }() + case 48: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .removeIgnoredNode(v) + } + }() case 64: try { var v: Bool? try decoder.decodeSingularBoolField(value: &v) @@ -1804,6 +1856,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .storeUiConfig(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 46) }() + case .setIgnoredNode?: try { + guard case .setIgnoredNode(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 47) + }() + case .removeIgnoredNode?: try { + guard case .removeIgnoredNode(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 48) + }() case .beginEditSettings?: try { guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 64) diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift index f677d644..6c4dbe93 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -134,6 +134,10 @@ public enum Language: SwiftProtobuf.Enum { /// Greek case greek // = 13 + /// + /// Norwegian + case norwegian // = 14 + /// /// Simplified Chinese (experimental) case simplifiedChinese // = 30 @@ -163,6 +167,7 @@ public enum Language: SwiftProtobuf.Enum { case 11: self = .russian case 12: self = .dutch case 13: self = .greek + case 14: self = .norwegian case 30: self = .simplifiedChinese case 31: self = .traditionalChinese default: self = .UNRECOGNIZED(rawValue) @@ -185,6 +190,7 @@ public enum Language: SwiftProtobuf.Enum { case .russian: return 11 case .dutch: return 12 case .greek: return 13 + case .norwegian: return 14 case .simplifiedChinese: return 30 case .traditionalChinese: return 31 case .UNRECOGNIZED(let i): return i @@ -212,6 +218,7 @@ extension Language: CaseIterable { .russian, .dutch, .greek, + .norwegian, .simplifiedChinese, .traditionalChinese, ] @@ -315,6 +322,13 @@ public struct DeviceUIConfig { /// Clears the value of `nodeHighlight`. Subsequent reads from it will return its default value. public mutating func clearNodeHighlight() {_uniqueStorage()._nodeHighlight = nil} + /// + /// 8 integers for screen calibration data + public var calibrationData: Data { + get {return _storage._calibrationData} + set {_uniqueStorage()._calibrationData = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -422,6 +436,7 @@ extension Language: SwiftProtobuf._ProtoNameProviding { 11: .same(proto: "RUSSIAN"), 12: .same(proto: "DUTCH"), 13: .same(proto: "GREEK"), + 14: .same(proto: "NORWEGIAN"), 30: .same(proto: "SIMPLIFIED_CHINESE"), 31: .same(proto: "TRADITIONAL_CHINESE"), ] @@ -443,6 +458,7 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 11: .same(proto: "language"), 12: .standard(proto: "node_filter"), 13: .standard(proto: "node_highlight"), + 14: .standard(proto: "calibration_data"), ] fileprivate class _StorageClass { @@ -459,6 +475,7 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement var _language: Language = .english var _nodeFilter: NodeFilter? = nil var _nodeHighlight: NodeHighlight? = nil + var _calibrationData: Data = Data() #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -486,6 +503,7 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement _language = source._language _nodeFilter = source._nodeFilter _nodeHighlight = source._nodeHighlight + _calibrationData = source._calibrationData } } @@ -517,6 +535,7 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 11: try { try decoder.decodeSingularEnumField(value: &_storage._language) }() case 12: try { try decoder.decodeSingularMessageField(value: &_storage._nodeFilter) }() case 13: try { try decoder.decodeSingularMessageField(value: &_storage._nodeHighlight) }() + case 14: try { try decoder.decodeSingularBytesField(value: &_storage._calibrationData) }() default: break } } @@ -568,6 +587,9 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement try { if let v = _storage._nodeHighlight { try visitor.visitSingularMessageField(value: v, fieldNumber: 13) } }() + if !_storage._calibrationData.isEmpty { + try visitor.visitSingularBytesField(value: _storage._calibrationData, fieldNumber: 14) + } } try unknownFields.traverse(visitor: &visitor) } @@ -590,6 +612,7 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if _storage._language != rhs_storage._language {return false} if _storage._nodeFilter != rhs_storage._nodeFilter {return false} if _storage._nodeHighlight != rhs_storage._nodeHighlight {return false} + if _storage._calibrationData != rhs_storage._calibrationData {return false} return true } if !storagesAreEqual {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 3349c2c9..34a33373 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -196,6 +196,21 @@ public struct NodeInfoLite { set {_uniqueStorage()._isFavorite = newValue} } + /// + /// True if node is in our ignored list + /// Persists between NodeDB internal clean ups + public var isIgnored: Bool { + get {return _storage._isIgnored} + set {_uniqueStorage()._isIgnored = newValue} + } + + /// + /// Last byte of the node number of the node that should be used as the next hop to reach this node. + public var nextHop: UInt32 { + get {return _storage._nextHop} + set {_uniqueStorage()._nextHop = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -486,6 +501,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 8: .standard(proto: "via_mqtt"), 9: .standard(proto: "hops_away"), 10: .standard(proto: "is_favorite"), + 11: .standard(proto: "is_ignored"), + 12: .standard(proto: "next_hop"), ] fileprivate class _StorageClass { @@ -499,6 +516,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _viaMqtt: Bool = false var _hopsAway: UInt32? = nil var _isFavorite: Bool = false + var _isIgnored: Bool = false + var _nextHop: UInt32 = 0 #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -523,6 +542,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway _isFavorite = source._isFavorite + _isIgnored = source._isIgnored + _nextHop = source._nextHop } } @@ -551,6 +572,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() 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.decodeSingularUInt32Field(value: &_storage._nextHop) }() default: break } } @@ -593,6 +616,12 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._isFavorite != false { try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) } + if _storage._isIgnored != false { + try visitor.visitSingularBoolField(value: _storage._isIgnored, fieldNumber: 11) + } + if _storage._nextHop != 0 { + try visitor.visitSingularUInt32Field(value: _storage._nextHop, fieldNumber: 12) + } } try unknownFields.traverse(visitor: &visitor) } @@ -612,6 +641,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._viaMqtt != rhs_storage._viaMqtt {return false} 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._nextHop != rhs_storage._nextHop {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 154d8a6b..434d1ec9 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -380,6 +380,11 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Lilygo TLora-C6 with the new ESP32-C6 MCU case tloraC6 // = 83 + /// + /// WisMesh Tap + /// RAK-4631 w/ TFT in injection modled case + case wismeshTap // = 84 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -477,6 +482,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 81: self = .seeedXiaoS3 case 82: self = .ms24Sf1 case 83: self = .tloraC6 + case 84: self = .wismeshTap case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -568,6 +574,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .seeedXiaoS3: return 81 case .ms24Sf1: return 82 case .tloraC6: return 83 + case .wismeshTap: return 84 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -664,6 +671,7 @@ extension HardwareModel: CaseIterable { .seeedXiaoS3, .ms24Sf1, .tloraC6, + .wismeshTap, .privateHw, ] } @@ -684,7 +692,7 @@ public enum Constants: SwiftProtobuf.Enum { /// From mesh.options /// note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is /// outside of this envelope - case dataPayloadLen // = 237 + case dataPayloadLen // = 233 case UNRECOGNIZED(Int) public init() { @@ -694,7 +702,7 @@ public enum Constants: SwiftProtobuf.Enum { public init?(rawValue: Int) { switch rawValue { case 0: self = .zero - case 237: self = .dataPayloadLen + case 233: self = .dataPayloadLen default: self = .UNRECOGNIZED(rawValue) } } @@ -702,7 +710,7 @@ public enum Constants: SwiftProtobuf.Enum { public var rawValue: Int { switch self { case .zero: return 0 - case .dataPayloadLen: return 237 + case .dataPayloadLen: return 233 case .UNRECOGNIZED(let i): return i } } @@ -2055,6 +2063,22 @@ public struct MeshPacket { set {_uniqueStorage()._pkiEncrypted = newValue} } + /// + /// Last byte of the node number of the node that should be used as the next hop in routing. + /// Set by the firmware internally, clients are not supposed to set this. + public var nextHop: UInt32 { + get {return _storage._nextHop} + set {_uniqueStorage()._nextHop = newValue} + } + + /// + /// Last byte of the node number of the node that will relay/relayed this packet. + /// Set by the firmware internally, clients are not supposed to set this. + public var relayNode: UInt32 { + get {return _storage._relayNode} + set {_uniqueStorage()._relayNode = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_PayloadVariant: Equatable { @@ -2370,6 +2394,14 @@ public struct NodeInfo { set {_uniqueStorage()._isFavorite = newValue} } + /// + /// True if node is in our ignored list + /// Persists between NodeDB internal clean ups + public var isIgnored: Bool { + get {return _storage._isIgnored} + set {_uniqueStorage()._isIgnored = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -3526,6 +3558,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 81: .same(proto: "SEEED_XIAO_S3"), 82: .same(proto: "MS24SF1"), 83: .same(proto: "TLORA_C6"), + 84: .same(proto: "WISMESH_TAP"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3533,7 +3566,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { extension Constants: SwiftProtobuf._ProtoNameProviding { public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 0: .same(proto: "ZERO"), - 237: .same(proto: "DATA_PAYLOAD_LEN"), + 233: .same(proto: "DATA_PAYLOAD_LEN"), ] } @@ -4328,6 +4361,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 15: .standard(proto: "hop_start"), 16: .standard(proto: "public_key"), 17: .standard(proto: "pki_encrypted"), + 18: .standard(proto: "next_hop"), + 19: .standard(proto: "relay_node"), ] fileprivate class _StorageClass { @@ -4347,6 +4382,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _hopStart: UInt32 = 0 var _publicKey: Data = Data() var _pkiEncrypted: Bool = false + var _nextHop: UInt32 = 0 + var _relayNode: UInt32 = 0 #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -4377,6 +4414,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio _hopStart = source._hopStart _publicKey = source._publicKey _pkiEncrypted = source._pkiEncrypted + _nextHop = source._nextHop + _relayNode = source._relayNode } } @@ -4431,6 +4470,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 15: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopStart) }() case 16: try { try decoder.decodeSingularBytesField(value: &_storage._publicKey) }() case 17: try { try decoder.decodeSingularBoolField(value: &_storage._pkiEncrypted) }() + case 18: try { try decoder.decodeSingularUInt32Field(value: &_storage._nextHop) }() + case 19: try { try decoder.decodeSingularUInt32Field(value: &_storage._relayNode) }() default: break } } @@ -4499,6 +4540,12 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._pkiEncrypted != false { try visitor.visitSingularBoolField(value: _storage._pkiEncrypted, fieldNumber: 17) } + if _storage._nextHop != 0 { + try visitor.visitSingularUInt32Field(value: _storage._nextHop, fieldNumber: 18) + } + if _storage._relayNode != 0 { + try visitor.visitSingularUInt32Field(value: _storage._relayNode, fieldNumber: 19) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4524,6 +4571,8 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._hopStart != rhs_storage._hopStart {return false} if _storage._publicKey != rhs_storage._publicKey {return false} if _storage._pkiEncrypted != rhs_storage._pkiEncrypted {return false} + if _storage._nextHop != rhs_storage._nextHop {return false} + if _storage._relayNode != rhs_storage._relayNode {return false} return true } if !storagesAreEqual {return false} @@ -4568,6 +4617,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 8: .standard(proto: "via_mqtt"), 9: .standard(proto: "hops_away"), 10: .standard(proto: "is_favorite"), + 11: .standard(proto: "is_ignored"), ] fileprivate class _StorageClass { @@ -4581,6 +4631,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _viaMqtt: Bool = false var _hopsAway: UInt32? = nil var _isFavorite: Bool = false + var _isIgnored: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -4605,6 +4656,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway _isFavorite = source._isFavorite + _isIgnored = source._isIgnored } } @@ -4633,6 +4685,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() 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) }() default: break } } @@ -4675,6 +4728,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._isFavorite != false { try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10) } + if _storage._isIgnored != false { + try visitor.visitSingularBoolField(value: _storage._isIgnored, fieldNumber: 11) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4694,6 +4750,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._viaMqtt != rhs_storage._viaMqtt {return false} if _storage._hopsAway != rhs_storage._hopsAway {return false} if _storage._isFavorite != rhs_storage._isFavorite {return false} + if _storage._isIgnored != rhs_storage._isIgnored {return false} return true } if !storagesAreEqual {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index e67b5272..1ce50e07 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -156,6 +156,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// SCD40/SCD41 CO2, humidity, temperature sensor case scd4X // = 32 + + /// + /// ClimateGuard RadSens, radiation, Geiger-Muller Tube + case radsens // = 33 case UNRECOGNIZED(Int) public init() { @@ -197,6 +201,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 30: self = .max30102 case 31: self = .mlx90614 case 32: self = .scd4X + case 33: self = .radsens default: self = .UNRECOGNIZED(rawValue) } } @@ -236,6 +241,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .max30102: return 30 case .mlx90614: return 31 case .scd4X: return 32 + case .radsens: return 33 case .UNRECOGNIZED(let i): return i } } @@ -280,6 +286,7 @@ extension TelemetrySensorType: CaseIterable { .max30102, .mlx90614, .scd4X, + .radsens, ] } @@ -554,6 +561,17 @@ public struct EnvironmentMetrics { /// Clears the value of `windLull`. Subsequent reads from it will return its default value. public mutating func clearWindLull() {_uniqueStorage()._windLull = nil} + /// + /// Radiation in µR/h + public var radiation: Float { + get {return _storage._radiation ?? 0} + set {_uniqueStorage()._radiation = newValue} + } + /// Returns true if `radiation` has been explicitly set. + public var hasRadiation: Bool {return _storage._radiation != nil} + /// Clears the value of `radiation`. Subsequent reads from it will return its default value. + public mutating func clearRadiation() {_uniqueStorage()._radiation = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1128,6 +1146,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 30: .same(proto: "MAX30102"), 31: .same(proto: "MLX90614"), 32: .same(proto: "SCD4X"), + 33: .same(proto: "RADSENS"), ] } @@ -1211,6 +1230,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 15: .same(proto: "weight"), 16: .standard(proto: "wind_gust"), 17: .standard(proto: "wind_lull"), + 18: .same(proto: "radiation"), ] fileprivate class _StorageClass { @@ -1231,6 +1251,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple var _weight: Float? = nil var _windGust: Float? = nil var _windLull: Float? = nil + var _radiation: Float? = nil #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -1262,6 +1283,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple _weight = source._weight _windGust = source._windGust _windLull = source._windLull + _radiation = source._radiation } } @@ -1297,6 +1319,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 15: try { try decoder.decodeSingularFloatField(value: &_storage._weight) }() case 16: try { try decoder.decodeSingularFloatField(value: &_storage._windGust) }() case 17: try { try decoder.decodeSingularFloatField(value: &_storage._windLull) }() + case 18: try { try decoder.decodeSingularFloatField(value: &_storage._radiation) }() default: break } } @@ -1360,6 +1383,9 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple try { if let v = _storage._windLull { try visitor.visitSingularFloatField(value: v, fieldNumber: 17) } }() + try { if let v = _storage._radiation { + try visitor.visitSingularFloatField(value: v, fieldNumber: 18) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -1386,6 +1412,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if _storage._weight != rhs_storage._weight {return false} if _storage._windGust != rhs_storage._windGust {return false} if _storage._windLull != rhs_storage._windLull {return false} + if _storage._radiation != rhs_storage._radiation {return false} return true } if !storagesAreEqual {return false} diff --git a/protobufs b/protobufs index 04f21f5c..02e6576e 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 04f21f5c7238b8e02f794d9282c4786752634b3c +Subproject commit 02e6576efaa2f691be9504b8c1c6261703f7a277 From e200a7ab49a7d019fef59cd7a60a55b0aea676f6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 29 Nov 2024 13:15:46 -0800 Subject: [PATCH 306/333] Update device hardware and admin channel config logging --- Meshtastic.xcodeproj/project.pbxproj | 8 ++--- Meshtastic/Resources/DeviceHardware.json | 34 +++++++++---------- .../Settings/Config/BluetoothConfig.swift | 4 ++- .../Views/Settings/Config/DeviceConfig.swift | 5 +-- .../Views/Settings/Config/DisplayConfig.swift | 3 +- .../Views/Settings/Config/LoRaConfig.swift | 4 ++- .../Config/Module/AmbientLightingConfig.swift | 3 +- .../Config/Module/CannedMessagesConfig.swift | 3 +- .../Config/Module/DetectionSensorConfig.swift | 3 +- .../Module/ExternalNotificationConfig.swift | 3 +- .../Settings/Config/Module/MQTTConfig.swift | 3 +- .../Config/Module/PaxCounterConfig.swift | 3 +- .../Config/Module/RangeTestConfig.swift | 3 +- .../Settings/Config/Module/RtttlConfig.swift | 3 +- .../Settings/Config/Module/SerialConfig.swift | 3 +- .../Config/Module/StoreForwardConfig.swift | 3 +- .../Config/Module/TelemetryConfig.swift | 3 +- .../Views/Settings/Config/NetworkConfig.swift | 3 +- .../Settings/Config/PositionConfig.swift | 3 +- .../Views/Settings/Config/PowerConfig.swift | 4 ++- .../Settings/Config/SecurityConfig.swift | 5 ++- 21 files changed, 65 insertions(+), 41 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6ea81c15..35592c2d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1704,7 +1704,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.10; + MARKETING_VERSION = 2.5.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1738,7 +1738,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.10; + MARKETING_VERSION = 2.5.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1770,7 +1770,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.10; + MARKETING_VERSION = 2.5.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1803,7 +1803,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.10; + MARKETING_VERSION = 2.5.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 94c74660..21784122 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -26,7 +26,7 @@ "hwModelSlug": "TLORA_V2_1_1P6", "platformioTarget": "tlora-v2-1-1_6", "architecture": "esp32", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "LILYGO T-LoRa V2.1-1.6", "tags": [ @@ -41,7 +41,7 @@ "hwModelSlug": "TBEAM", "platformioTarget": "tbeam", "architecture": "esp32", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "LILYGO T-Beam", "tags": [ @@ -79,7 +79,7 @@ "platformioTarget": "t-echo", "architecture": "nrf52840", "supportLevel": 1, - "activelySupported": true, + "activelySupported": false, "displayName": "LILYGO T-Echo", "tags": [ "LilyGo" @@ -392,7 +392,7 @@ "hwModelSlug": "HELTEC_V3", "platformioTarget": "heltec-v3", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec V3", "tags": [ @@ -408,7 +408,7 @@ "hwModelSlug": "HELTEC_WSL_V3", "platformioTarget": "heltec-wsl-v3", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Wireless Stick Lite V3", "tags": [ @@ -451,7 +451,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "heltec-wireless-tracker", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Wireless Tracker V1.1", "tags": [ @@ -480,7 +480,7 @@ "hwModelSlug": "HELTEC_WIRELESS_PAPER", "platformioTarget": "heltec-wireless-paper", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Wireless Paper", "tags": [ @@ -495,7 +495,7 @@ "hwModelSlug": "T_DECK", "platformioTarget": "t-deck", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "LILYGO T-Deck", "tags": [ @@ -511,7 +511,7 @@ "hwModelSlug": "T_WATCH_S3", "platformioTarget": "t-watch-s3", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "LILYGO T-Watch S3", "tags": [ @@ -536,7 +536,7 @@ "platformioTarget": "heltec-ht62-esp32c3-sx1262", "architecture": "esp32-c3", "supportLevel": 1, - "activelySupported": true, + "activelySupported": false, "displayName": "Heltec HT62", "tags": [ "Heltec" @@ -575,7 +575,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 3, "displayName": "TrackSenger (small TFT)", "requiresDfu": true @@ -585,7 +585,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger-lcd", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 3, "displayName": "TrackSenger (big TFT)", "requiresDfu": true @@ -595,7 +595,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger-oled", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 3, "displayName": "TrackSenger (big OLED)" }, @@ -629,7 +629,7 @@ "hwModelSlug": "HELTEC_VISION_MASTER_T190", "platformioTarget": "heltec-vision-master-t190", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Vision Master T190", "tags": [ @@ -645,7 +645,7 @@ "hwModelSlug": "HELTEC_VISION_MASTER_E213", "platformioTarget": "heltec-vision-master-e213", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Vision Master E213", "tags": [ @@ -661,7 +661,7 @@ "hwModelSlug": "HELTEC_VISION_MASTER_E290", "platformioTarget": "heltec-vision-master-e290", "architecture": "esp32-s3", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Vision Master E290", "tags": [ @@ -677,7 +677,7 @@ "hwModelSlug": "HELTEC_MESH_NODE_T114", "platformioTarget": "heltec-mesh-node-t114", "architecture": "nrf52840", - "activelySupported": true, + "activelySupported": false, "supportLevel": 1, "displayName": "Heltec Mesh Node T114", "tags": [ diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 37f989a5..d57dacba 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -103,7 +103,7 @@ struct BluetoothConfig: View { .onFirstAppear { // Need to request a BluetoothConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty bluetooth config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -111,10 +111,12 @@ struct BluetoothConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.bluetoothConfig == nil { + Logger.mesh.info("⚙️ Empty or expired bluetooth config requesting via PKI admin") _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty bluetooth config") _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index f5fca283..da2978cf 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -78,7 +78,7 @@ struct DeviceConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $tripleClickAsAdHocPing) { - Label("Triple Click Ad Hoc Ping", systemImage: "map.pin") + Label("Triple Click Ad Hoc Ping", systemImage: "mappin") Text("Send a position on the primary channel when the user button is triple clicked.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -229,7 +229,6 @@ struct DeviceConfig: View { .onFirstAppear { // Need to request a DeviceConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty device config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -237,11 +236,13 @@ struct DeviceConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.deviceConfig == nil { + Logger.mesh.info("⚙️ Empty or expired device config requesting via PKI admin") _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { if node.deviceConfig == nil { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty device config") _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 07975419..88839956 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -166,7 +166,6 @@ struct DisplayConfig: View { .onFirstAppear { // Need to request a DisplayConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty display config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -174,10 +173,12 @@ struct DisplayConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.displayConfig == nil { + Logger.mesh.info("⚙️ Empty or expired display config requesting via PKI admin") _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty display config") _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 4c12c598..51461cf0 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -241,7 +241,6 @@ struct LoRaConfig: View { .onFirstAppear { // Need to request a LoRaConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty lora config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -249,10 +248,13 @@ struct LoRaConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.loRaConfig == nil { + Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin") + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty lora config") _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index fc82a4ca..62340da8 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -88,7 +88,6 @@ struct AmbientLightingConfig: View { .onFirstAppear { // Need to request a Ambient Lighting Config from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty ambient lighting config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -96,10 +95,12 @@ struct AmbientLightingConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.ambientLightingConfig == nil { + Logger.mesh.info("⚙️ Empty or expired ambient lighting module config requesting via PKI admin") _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty ambient lighting module config") _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 4f580b2a..b5dfee63 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -236,7 +236,6 @@ struct CannedMessagesConfig: View { .onFirstAppear { // Need to request a CannedMessagesModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty canned message config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -244,10 +243,12 @@ struct CannedMessagesConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.cannedMessageConfig == nil { + Logger.mesh.info("⚙️ Empty or expired canned messages module config requesting via PKI admin") _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty canned messages module config") _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index aff3dfea..59ee4f3f 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -194,7 +194,6 @@ struct DetectionSensorConfig: View { .onFirstAppear { // Need to request a DetectionSensorModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty detection sensor config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -202,10 +201,12 @@ struct DetectionSensorConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.detectionSensorConfig == nil { + Logger.mesh.info("⚙️ Empty or expired detection sensor module config requesting via PKI admin") _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty detection sensor module config") _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index a0f14c7f..9602c44b 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -202,7 +202,6 @@ struct ExternalNotificationConfig: View { .onFirstAppear { // Need to request a ExternalNotificationModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty external notificaiton module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -210,10 +209,12 @@ struct ExternalNotificationConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.externalNotificationConfig == nil { + Logger.mesh.info("⚙️ Empty or expired external notificaiton module config requesting via PKI admin") _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty external notificaiton module config") _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 224fd43c..6694097f 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -322,7 +322,6 @@ struct MQTTConfig: View { .onFirstAppear { // Need to request a MqttModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty mqtt module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -330,10 +329,12 @@ struct MQTTConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.mqttConfig == nil { + Logger.mesh.info("⚙️ Empty or expired mqtt module config requesting via PKI admin") _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty mqtt module config") _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 2fd97646..24af3504 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -61,7 +61,6 @@ struct PaxCounterConfig: View { .onFirstAppear { // Need to request a PaxCounterModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty pax counter module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -69,10 +68,12 @@ struct PaxCounterConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.paxCounterConfig == nil { + Logger.mesh.info("⚙️ Empty or expired pax counter module config requesting via PKI admin") _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty pax counter module config") _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index ae4797fc..979eb736 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -84,7 +84,6 @@ struct RangeTestConfig: View { .onFirstAppear { // Need to request a RangeTestModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty range test module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -92,10 +91,12 @@ struct RangeTestConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.rangeTestConfig == nil { + Logger.mesh.info("⚙️ Empty or expired range test module config requesting via PKI admin") _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty range test module config") _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 2e0931a7..b81e2348 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -75,7 +75,6 @@ struct RtttlConfig: View { .onFirstAppear { // Need to request a RtttlConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty range test module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -83,10 +82,12 @@ struct RtttlConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.rtttlConfig == nil { + Logger.mesh.info("⚙️ Empty or expired ringtone module config requesting via PKI admin") _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty ringtone module config") _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index a7fd5bdb..fd2c0c99 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -139,7 +139,6 @@ struct SerialConfig: View { .onFirstAppear { // Need to request a SerialModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty serial module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -147,10 +146,12 @@ struct SerialConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.serialConfig == nil { + Logger.mesh.info("⚙️ Empty or expired serial module config requesting via PKI admin") _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty serial module config") _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index d90ea3fb..9c247acf 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -149,7 +149,6 @@ struct StoreForwardConfig: View { .onFirstAppear { // Need to request a StoreForwardModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty store & forward module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -157,10 +156,12 @@ struct StoreForwardConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.storeForwardConfig == nil { + Logger.mesh.info("⚙️ Empty or expired store & forward module config requesting via PKI admin") _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty store & forward module config") _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 4ba39834..ad173dae 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -137,7 +137,6 @@ struct TelemetryConfig: View { .onFirstAppear { // Need to request a TelemetryModuleConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty telemetry module config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -145,10 +144,12 @@ struct TelemetryConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.telemetryConfig == nil { + Logger.mesh.info("⚙️ Empty or expired telemetry module config requesting via PKI admin") _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty telemetry module config") _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 8cebde85..84f48c41 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -133,7 +133,6 @@ struct NetworkConfig: View { .onFirstAppear { // Need to request a NetworkConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty network config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -141,10 +140,12 @@ struct NetworkConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.networkConfig == nil { + Logger.mesh.info("⚙️ Empty or expired network config requesting via PKI admin") _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty network config") _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 451ac02d..f963b02b 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -410,7 +410,6 @@ struct PositionConfig: View { supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame // Need to request a NetworkConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty position config") let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -418,10 +417,12 @@ struct PositionConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.positionConfig == nil { + Logger.mesh.info("⚙️ Empty or expired position config requesting via PKI admin") _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty position config") _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 822fd1e0..e9f7c0e5 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -130,7 +130,7 @@ struct PowerConfig: View { } // Need to request a NetworkConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty power config") + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -138,10 +138,12 @@ struct PowerConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.powerConfig == nil { + Logger.mesh.info("⚙️ Empty or expired power config requesting via PKI admin") _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty power config") _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 5fb391b3..e62251c8 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -198,7 +198,8 @@ struct SecurityConfig: View { .onFirstAppear { // Need to request a DeviceConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { - Logger.mesh.info("empty security config") + + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { if node.num != connectedNode.num { @@ -206,11 +207,13 @@ struct SecurityConfig: View { /// 2.5 Administration with session passkey let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.securityConfig == nil { + Logger.mesh.info("⚙️ Empty or expired security config requesting via PKI admin") _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } else { if node.deviceConfig == nil { /// Legacy Administration + Logger.mesh.info("☠️ Using insecure legacy admin, empty security config") _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } From 8d810ec22dd200ee763555b33998e267d563328d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 2 Dec 2024 18:09:49 -0800 Subject: [PATCH 307/333] update hardware list --- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Resources/DeviceHardware.json | 34 +++++++++---------- .../Views/Bluetooth/InvalidVersion.swift | 4 +-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index bb707885..db1f1e09 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.0.0" + public var minimumVersion = "2.5.0" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 21784122..94c74660 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -26,7 +26,7 @@ "hwModelSlug": "TLORA_V2_1_1P6", "platformioTarget": "tlora-v2-1-1_6", "architecture": "esp32", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "LILYGO T-LoRa V2.1-1.6", "tags": [ @@ -41,7 +41,7 @@ "hwModelSlug": "TBEAM", "platformioTarget": "tbeam", "architecture": "esp32", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "LILYGO T-Beam", "tags": [ @@ -79,7 +79,7 @@ "platformioTarget": "t-echo", "architecture": "nrf52840", "supportLevel": 1, - "activelySupported": false, + "activelySupported": true, "displayName": "LILYGO T-Echo", "tags": [ "LilyGo" @@ -392,7 +392,7 @@ "hwModelSlug": "HELTEC_V3", "platformioTarget": "heltec-v3", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec V3", "tags": [ @@ -408,7 +408,7 @@ "hwModelSlug": "HELTEC_WSL_V3", "platformioTarget": "heltec-wsl-v3", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Wireless Stick Lite V3", "tags": [ @@ -451,7 +451,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "heltec-wireless-tracker", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Wireless Tracker V1.1", "tags": [ @@ -480,7 +480,7 @@ "hwModelSlug": "HELTEC_WIRELESS_PAPER", "platformioTarget": "heltec-wireless-paper", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Wireless Paper", "tags": [ @@ -495,7 +495,7 @@ "hwModelSlug": "T_DECK", "platformioTarget": "t-deck", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "LILYGO T-Deck", "tags": [ @@ -511,7 +511,7 @@ "hwModelSlug": "T_WATCH_S3", "platformioTarget": "t-watch-s3", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "LILYGO T-Watch S3", "tags": [ @@ -536,7 +536,7 @@ "platformioTarget": "heltec-ht62-esp32c3-sx1262", "architecture": "esp32-c3", "supportLevel": 1, - "activelySupported": false, + "activelySupported": true, "displayName": "Heltec HT62", "tags": [ "Heltec" @@ -575,7 +575,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (small TFT)", "requiresDfu": true @@ -585,7 +585,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger-lcd", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (big TFT)", "requiresDfu": true @@ -595,7 +595,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger-oled", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (big OLED)" }, @@ -629,7 +629,7 @@ "hwModelSlug": "HELTEC_VISION_MASTER_T190", "platformioTarget": "heltec-vision-master-t190", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Vision Master T190", "tags": [ @@ -645,7 +645,7 @@ "hwModelSlug": "HELTEC_VISION_MASTER_E213", "platformioTarget": "heltec-vision-master-e213", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Vision Master E213", "tags": [ @@ -661,7 +661,7 @@ "hwModelSlug": "HELTEC_VISION_MASTER_E290", "platformioTarget": "heltec-vision-master-e290", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Vision Master E290", "tags": [ @@ -677,7 +677,7 @@ "hwModelSlug": "HELTEC_MESH_NODE_T114", "platformioTarget": "heltec-mesh-node-t114", "architecture": "nrf52840", - "activelySupported": false, + "activelySupported": true, "supportLevel": 1, "displayName": "Heltec Mesh Node T114", "tags": [ diff --git a/Meshtastic/Views/Bluetooth/InvalidVersion.swift b/Meshtastic/Views/Bluetooth/InvalidVersion.swift index 9c0cca78..24b5429c 100644 --- a/Meshtastic/Views/Bluetooth/InvalidVersion.swift +++ b/Meshtastic/Views/Bluetooth/InvalidVersion.swift @@ -37,11 +37,11 @@ struct InvalidVersion: View { Divider() .padding(.top) VStack { - Text("🦕 End of life Version 🦖 ☄️") + Text("Insecure legacy firmware version") .font(.title3) .foregroundColor(.orange) .padding(.bottom) - Text("Version \(minimumVersion) includes breaking changes to devices and the client apps. Only nodes version \(minimumVersion) and above are supported.") + Text("Version \(minimumVersion) contains substantial security vulnerabilities, you should upgrade to a newer version immediatly. Only nodes version \(minimumVersion) and above are supported.") .font(.callout) .padding([.leading, .trailing, .bottom]) Link("Version 1.2 End of life (EOL) Info", destination: URL(string: "https://meshtastic.org/docs/1.2-End-of-life/")!) From 93dd78bff8dde30f5e207b8754034538e43d04c9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 2 Dec 2024 20:30:55 -0800 Subject: [PATCH 308/333] Revert firmare nag screen updates --- Meshtastic/Views/Bluetooth/InvalidVersion.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Bluetooth/InvalidVersion.swift b/Meshtastic/Views/Bluetooth/InvalidVersion.swift index 24b5429c..9c0cca78 100644 --- a/Meshtastic/Views/Bluetooth/InvalidVersion.swift +++ b/Meshtastic/Views/Bluetooth/InvalidVersion.swift @@ -37,11 +37,11 @@ struct InvalidVersion: View { Divider() .padding(.top) VStack { - Text("Insecure legacy firmware version") + Text("🦕 End of life Version 🦖 ☄️") .font(.title3) .foregroundColor(.orange) .padding(.bottom) - Text("Version \(minimumVersion) contains substantial security vulnerabilities, you should upgrade to a newer version immediatly. Only nodes version \(minimumVersion) and above are supported.") + Text("Version \(minimumVersion) includes breaking changes to devices and the client apps. Only nodes version \(minimumVersion) and above are supported.") .font(.callout) .padding([.leading, .trailing, .bottom]) Link("Version 1.2 End of life (EOL) Info", destination: URL(string: "https://meshtastic.org/docs/1.2-End-of-life/")!) From cf2355c008d391192e2cf858bc6a1feff11a6499 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 3 Dec 2024 07:56:31 -0800 Subject: [PATCH 309/333] Bump minimum version to match android --- Meshtastic/Helpers/BLEManager.swift | 2 +- Meshtastic/Views/Bluetooth/InvalidVersion.swift | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index db1f1e09..90d9f978 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.0" + public var minimumVersion = "2.3.2" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false diff --git a/Meshtastic/Views/Bluetooth/InvalidVersion.swift b/Meshtastic/Views/Bluetooth/InvalidVersion.swift index 9c0cca78..291722f4 100644 --- a/Meshtastic/Views/Bluetooth/InvalidVersion.swift +++ b/Meshtastic/Views/Bluetooth/InvalidVersion.swift @@ -44,8 +44,6 @@ struct InvalidVersion: View { Text("Version \(minimumVersion) includes breaking changes to devices and the client apps. Only nodes version \(minimumVersion) and above are supported.") .font(.callout) .padding([.leading, .trailing, .bottom]) - Link("Version 1.2 End of life (EOL) Info", destination: URL(string: "https://meshtastic.org/docs/1.2-End-of-life/")!) - .font(.callout) #if targetEnvironment(macCatalyst) Button { From 5c443509b3fc2587464a5b699c9d1164b6bfaed0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 3 Dec 2024 17:23:31 -0800 Subject: [PATCH 310/333] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9d083bd..bbaf49b0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SwiftUI client applications for iOS, iPadOS and macOS. ## Getting Started -This project is currently using **Xcode 15.4**. +This project always uses the latest release version of XCode. 1. Clone the repo. 2. Set up git hooks to automatically lint the project when you commit changes. From d9442d3b4ef305785c0fceb45e5d8ff8b4144a07 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Dec 2024 20:34:25 -0800 Subject: [PATCH 311/333] Device image updates --- Localizable.xcstrings | 9 +- .../HELTECV3.imageset/Contents.json | 2 +- .../HELTECV3.imageset/Heltec_turq.png | Bin 4401012 -> 0 bytes .../HELTECV3.imageset/heltec-v3-case.svg | 1 + .../LILYGOTBEAMS3CORE.imageset/Contents.json | 12 -- .../tbeam_supreme.png | Bin 4253168 -> 0 bytes .../NANOG2ULTRA.imageset/Contents.json | 2 +- .../NANOG2ULTRA.imageset/nano-g2-ultra.svg | 1 + .../nano_g2_ultra_product_image.jpg | Bin 495311 -> 0 bytes .../Contents.json | 2 +- .../STATIONG2.imageset/station-g2.svg | 1 + .../Assets.xcassets/TBEAM.imageset/tbeam.png | Bin 1768066 -> 0 bytes .../TECHO.imageset/Contents.json | 12 -- ...-868-915MHz-Wireless-Module-L76K-GPS-1.png | Bin 440392 -> 0 bytes .../TLORABOARD.imageset/Contents.json | 12 -- ...ESP32-LoRa-OLED-0-96.jpg_Q90.jpg_.webp.png | Bin 468219 -> 0 bytes .../TLORAV1.imageset/Contents.json | 12 -- .../TLORAV1.imageset/TLORA_olive 1.png | Bin 4192437 -> 0 bytes .../UNSET.imageset/Contents.json | 2 +- .../play_store_icon_114px-2.png | Bin 11866 -> 0 bytes .../UNSET.imageset/unknown.svg | 189 ++++++++++++++++++ .../CoreData/UserEntityExtension.swift | 13 +- Meshtastic/Extensions/String.swift | 1 - Meshtastic/Helpers/MeshPackets.swift | 16 +- .../Views/Bluetooth/InvalidVersion.swift | 2 +- 25 files changed, 212 insertions(+), 77 deletions(-) delete mode 100644 Meshtastic/Assets.xcassets/HELTECV3.imageset/Heltec_turq.png create mode 100644 Meshtastic/Assets.xcassets/HELTECV3.imageset/heltec-v3-case.svg delete mode 100644 Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json delete mode 100644 Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.png create mode 100644 Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano-g2-ultra.svg delete mode 100644 Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano_g2_ultra_product_image.jpg rename Meshtastic/Assets.xcassets/{TBEAM.imageset => STATIONG2.imageset}/Contents.json (76%) create mode 100644 Meshtastic/Assets.xcassets/STATIONG2.imageset/station-g2.svg delete mode 100644 Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.png delete mode 100644 Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json delete mode 100644 Meshtastic/Assets.xcassets/TECHO.imageset/LILYGO-TTGO-SoftRF-T-Echo-NRF52840-LoRa-SX1262-433-868-915MHz-Wireless-Module-L76K-GPS-1.png delete mode 100644 Meshtastic/Assets.xcassets/TLORABOARD.imageset/Contents.json delete mode 100644 Meshtastic/Assets.xcassets/TLORABOARD.imageset/LILYGO-TTGO-LoRa32-V2-1-1-6-Version-433-868-915Mhz-ESP32-LoRa-OLED-0-96.jpg_Q90.jpg_.webp.png delete mode 100644 Meshtastic/Assets.xcassets/TLORAV1.imageset/Contents.json delete mode 100644 Meshtastic/Assets.xcassets/TLORAV1.imageset/TLORA_olive 1.png delete mode 100644 Meshtastic/Assets.xcassets/UNSET.imageset/play_store_icon_114px-2.png create mode 100644 Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2881b290..99a00727 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -23713,18 +23713,15 @@ } } }, - "Version %@ includes breaking changes to devices and the client apps. Only nodes version %@ and above are supported." : { + "Version %@ includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version %@ and above are supported." : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Version %1$@ includes breaking changes to devices and the client apps. Only nodes version %2$@ and above are supported." + "value" : "Version %1$@ includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version %2$@ and above are supported." } } } - }, - "Version 1.2 End of life (EOL) Info" : { - }, "Version: %@ (%@) " : { "localizations" : { @@ -24065,4 +24062,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json index 98595042..42c0472b 100644 --- a/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/HELTECV3.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Heltec_turq.png", + "filename" : "heltec-v3-case.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/HELTECV3.imageset/Heltec_turq.png b/Meshtastic/Assets.xcassets/HELTECV3.imageset/Heltec_turq.png deleted file mode 100644 index c4454bcc5b6d03823d6ab4f46d6d87c013fb6b97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4401012 zcmeFZc|6qZ_XmuSt&;31gCS%Y`;y&Q#=aJ^G#HG1mqPY^-?zxlSjrl*XD5mfl6{G^ z5VH0RCGPI--}nB0pV#a8=NYdtbItjjbFSr__c`afJ~I=fq9k(;j}i|J4ecC67OaMb zM$mNlkAsb*q?^Q6AYafO)MO;kO8ThYqoJMAfN4QY6cy2!kr!~#@X)c*&K$C!VIZF_ z96qBRO8TP{9<@o3?ND^0pFHSD9!fNi~lhfl|^;>I?HFo24gjiZf& znT;)glY@;Dz^Di{f?FR_n9$HTL7W_d9GrrjAOI&w5X2|Q&5MRc>z#Z&Mht)Nhrin4 zfwU)c3cU8S-HC=42s)WZAm^{~c1EJ1okflZ6U4~{iac{XCye+=%wKIpG&PY_?9uSfqebRn}{*xcs z+arwQaS}8=I&;#_JpW72Lgs~E?T?v+KV^~D&iXMg^g}1eLO+@#I#MNC_ObrMHpWR` z*=J6*kA3Hm=ZDSDA?LoC&3V)p($;4%v$>DPOowa(vw4o%_{g>_QsnUOuq$`uKha^E z6U4zM2;vb02>?J`g2)F4C(^2ary;G-g@%@lfrgIr6%CEL4Jmv0cc|;|@9@?5=(Vhd zO1EdOETUl)fG&DBkjM?bW@8*a)qoI)n!ZoxU zwGYh`KeAb3}p_J~6e**+w*(*llI94&-t zwG>qVk~a1*052Pe4MZz~2LJ$s?2SzX)xgp}#gXrXY0VrRZ3WrcU0ht)T)5b5>`mD@ z1Ox=wL7eQIoUBL=RtHyWN9bKvYX`a?N{;n_VGc(2a9c;XjWysckA;W(V{Uzb^ln#gXIv@CPBJl?Byd_Hd}Bl9QDo%wFU)%!Ce|;IEfYT-81H{_IMg)mbc8)`&BA-F*pu_(klD{~9>-LcT zmrgvPF&rdI&fXs;K{*52kW z+zRRlleRH(asa~}9HG`muoD&k$Z<;3Ar1v^nZX@l5>U${ab*90C;l!Wb=TI$!O0$` zXk!c$IhObf@plOo_+6N#HW-eC&!J^SxHx&afA;kIrQby%>dNZ2Fqp9#a*3=>9ZoGG z(*2G?pE}I$uhioUmT(bH5Yi0};~27iqWO^Fi27I2pCkI`9)*t92D#u+OBLjL!46Sn z>2zp1O>4NL26*$fNyGCTE<}f42zxwXc690bXcgbUCJDiLN2RjER&(Dtka^ZKW zqZuG^Z|Z3FBVL>y?N9peGLlXXjy6_mFl%F&J0L72xIJzf259TG$bO4>{yN*!bk)-xL_Z ztYMN++e1G)OdP)^tp59j-zAS%Ud7(V)E;W}%b|a#|1Kx}Gih+}9|pf40Q*V$omCNr zTqR@>IEp*R7XJ(JcZs7>AI;-f_X*{9-lK4QRP)$E?H!J0FHC!!H-8mLKXcxXaXMKW z9mQ>FsHFq!c<{%>6a9zb=7#|M(1-rQeMI>Ks~QYx0%S(`srBdEKQJCG#&P61dh-V+ zHJGuK>yLPSy7yD$6QSdI9HyXCG4uD>r}ft$zzyO7@p1k`5ID_ys-vSdIra|;IMm^X z^B-lwf4KI$^ilTE`sc)Q>eBC`Qpn`@v(gYb+|>P?_i6HPB2rp{5;n%Je<=$7TI(E+ z`cGVcS9G+IMFPdv3~r=;m=zu2&TvQ99|`NHnt$T?yEZi#vhGGES7W4|eCS+~>G)+zbF@cdoPsg*iH^-sS?F8S|Q{w{ks zJYnPi+ry0@*2uhPYmXfIQMN%k{gL!PbNsIBBvc>QsYeQaBmXXSx^z1k z^J(hu5{gg@*pGQj8ys#d2o-=D z8^Z(+-Omuj$_<5@unL%P@v=gVO*oLpGCZ6dP?ID7QB*uF{x{XK$nBGjz3ZuTcMQb; zBO?6ETH$9DIohfVI@p*v9&UEUO_A>NFI@jmQYWMTq55#BKlqHypw_0y3WHsU{jY?7 z7I86yS^r;w;D??gu76en|GQ-bxOt!)P+opkUK0UMR&G8{E>=Sh7?c&nEx-li;^yJw zF)}L9Ikoct9i)D^#VPUMZE)h0|E~@A z=Y01gCj1?^hY1Rab};f(L8Mm$W59Faz83^cwYzrk&@)) z5a8uyJvwbWQFMy$k9v-dDvov>$UW|f*a`WMQVK9rsF5o~MMDy~3xzp2ittH-xutof zxxu^~k`f>=Sb_({Da9qt$IU0e&n3+-ep>h6gnxGN!$nS=T^~jAA5`SYxCkE7lE2px1F4YhjvATX@wzMf7PH*z9&Fd!nIZVB!!0#83^? zBA`40g#r``P$)p50EGe+3Q#COp#X&f6beu%K%oGI0u%~RC_teAg#r``P$)p50EGe+ z3Q#COp#X&f6beu%K%oGI0u%~RC_teAg#r``P$)p50EGe+3Q#COp#X&f6beu%K%oGI z0u%~RC_teAg#r``P$)p5;9sJEaJ$It__qL1i}o)U4Yg3Hg+j$x6beu%_!A05{KU42 zLv&*P{rA4VP2&i;u1&G~m&9%?eC;;pu^b-U(U}~&Fk4Zhy|O&Puaxl2dXzQiHB5iA z1NLrtciIY5jxS6!x%T~&mF0VBd09F=DbDxP77N`C3>4L$-s-MPSebas?mj8nzg1t> z>-2G1!{SzajxZB7*LRa)_X?KvzA_f8XAz^jN?n1@jqZ<~?&_{^ntaN+y-+`7zG2YA zsn=QKyZ~#^=&`NqoqeUU8z8?r`FgCePe*cfIi%)x)zg6KY28};+2Q&J+tYj4^|(L3 zqex(a_|M!cog>+tCl< zI-9jQ=iLq7vFa5}lzj-BU9cV*AvLT?^6X9#IdjRgCK9K|dblQ%$K{f}Ycyex>H_t` z?#Ox_RfVVgaAUOln%i9sxh=coh1;uK{ra#ykM`B40L6BC+rf>TVX~m+uehakEVwU? zS1;5&NPOA*)XK7jm%aS^`+NEik~rAQ`nrTFzszS>p(z%##}tn)udEsBD!ce+JkX7| zwscL=St<0$SKcjpSgcg`VrOTS{rkrKfGWW8*Z!MahzLEml@zcq{@dRG@S<*Ln(f&7 zF47z&57*G%&~r`rREViu5-T`gZh7r>S&UBHL)S64S9nnS#|A_~km@?H=ODRvqo+U-S7^(x)kiBS%*>BLp#aMxAI)QJ*2)} zjl*BdV(#Cl*#fSAG|4eGsoko1`pmczQ&ayMqr(#(0)u(C2gzK*{epuw->Y;2bFp+d z7w!Av277Ony~|2{%+up0o4?2?o>aOzbm4ZRoahTgSUS<(`<3M{Z$G*`FNcz+0Y%np zdne$AqK_X>*6oH&zUy!~Mvj;AH2~WBd&^>le;rRn?t5Lk(6|#&(wh@+eP4IM$1Zgs zRZZE#T4{E{*`OyyAR~c|CbotJZkM!`02f@+nd}jw?vo#VttQGKAKl~1Udck8S7Mp- zEr;Pwv;u^J6Lh{uJo)wjqr-6Pz?h!(wT3n3=~()p_z{Z8CkidRIjIay(T~@2_T<+U zym@jOx^;}6^}R`eM|V|!7=589!Fo37Ssa(>i>o7f**2UE=eXp%9ju@%2i$+r(vxtW=KDUv37_f=|qI|0$ z$~Dc!n!UcfvV5I-#KZAZ`WTn!uOE>}mqtQtuv#bXUq=|Xc#H^Y(Qo6!zR`~$iZ6{T zM^g{@Cu0`#%|W9MuC&BAH%_6K%X&AXP0Qg~d(Oj|&!j5e24 zM!PAVjyXv%O)dx-D5yg~d37(|yj519f59ryWC(dc(H^p*i^~$)8YCL1XZT`w$ZnE(Dmiq%zA_1$61WfRGqDaV-ITn_f zm%Wa$^J|vy2aWuumiUi#AwAv0px1n}{04nv?k(51xE9SfZ&*Pe`X>vFHezWlY$o+W z+*4{s#_CxryD8FOeL1|mUJtqJ3E&P%mx>Bp(||prH!3_8-BSRHCqV{#?P)R+60}Q;*J)Oe5z$+ao}PbjBisKfJ!V z54!7~>NQN(?w3Chnr!)wdz}6c2+`3KMZx{L$Cy4|=YL$?&lLDn)DJdp?W^&wxYj21 zN>ApD8>e;DbJt{Vuj&>^m-8T}fxb3=1opahE zm%87ln_7NMaKgAYPO;VSqGvH=ST+XG^^DZAtIk({$5%DJ_(Q_t>cMLa_QJOo<5dn1 z{d9;PkG`%IgqSl-KYX7bRxDt!5(fE3zgw}zt~?&g%Wm4G)8o8ZGtl>HhJzx>K`FpB zRa<`HA>Y{E_p1V1&wB-yEw+qt^?dnYXvO7w z@#tkO)f~0V6@_@tIbi^C>m1>J z0v2X9(e3dzyeTh`CBZi*e^|HcZF&5{UWbQ7nGnyM)T*}%A1lMgX14Y1eb+zta68=? z&8w`^HM>3UF=n*9OX>`t=}I%<&Zuh(ygZA~sv&fHDm~KZiq`X%fwxmhvagA84X-G0v-yF}bUx4GcgwDRN z|B!^Hcw=XDTK-*Sgbe=5hmxCC1!Kh{D~mPnlvNg2-1_L>XL+zazP0~(5wjuYm6P7x`Igc++M9CQ{nopjJebQ^ zQG^^3I~*>cFqI{x%!}*#wsQ+pF!zSjdy(FpBBEt}1ul6V{n`mkB^|V!%6c~i{9bX1 zD|ua(z@>rYJ4EbBi~GIb2?IB`0(w%xGBbeHQD>1$ws4v#*Biy0WVZ|Dyt&B&G8#Is z-d@%3c|h=dImPbnTgaQa>IvnT?p;#L_qE9{d#We5Zg*_gC6^W!B|MTB?s6S(D5haR zPbh;o27cf(QMw_$neN=z5jH}k;5r;0S0wjNWWD_3TMQ1NyhgV!VtGWg!G6{7vu zzlfAx-n2eDxi`-r!6!GF4&=6HoeuGZ)XL*#P<_?gf-W!Y+fiNeEWq_;;SmpeD70&^ z`EK=A49koD;8~Zk8xGWKsY4S(LqiirmSPeP&F9E;0Ux4-&$N({R`C!GlLLF3(=IPy z10&Ez8%jfc`SF0j0}P-qp{`bZPIGAKxKRr9tzJM0qNumKxBIz>`y|=L>vugq?HDeW zq`|4jCN#At8kfdy&<;=QeY!`hHb(a4>vj07A=SV%h%}assUUV>(kPvg?Jj*Md*BtB zR*EQb;8XB;t9W!^_QiI9mn;!3h4kP>ISB@Fb9kES#f3r*VONGPemuaAuSF9L!Rp*| z^hwgCH}RT;Eyy(7qTXg2fxF@Qw4E#k1KzQNdbg-HY#)fT1?O3UpmMFG#Rli|+1)AI zNqqbb$=?{cdxe&d&DRGwuB&m77k^@G^WV6>CV6EBF!_)`y_rjuCRcFUO+|ASYsJ7c zT_c#H<^q#{H}|Z$NIKaE3NJI`htb`fLO!+Io}V@HA(AUW)$T@d=EUypASm@G|u)(qAhr-;9<{^y3*NOIHjfKT5I^~mT3 zdYpVO`Vw?e4B=|t{;1+Qk7bXgsf7)UkLqUHSZ|ozL$3%Y@|wH4uff*rR$K@ z@KP8-oVtb;{l~2Ol5ypWI>0zU#aEe5+AE^Vl42Qda+I4js~cIBT@UygS)#NywQL@? zk;wJW*=guBvxt{Kg~lafRY_8!HT#d|`a2mWl61+14}?MOA*4J3{M26oLy2 ze(q^};Twk|@(arw*dujc3hda4#u{yIxTT==ouo@M3(2{|lFI~=(3q?HXq<&=+1k!McishtD3VqeF zBlt9swx1hM0LDNAK5N(^KbMd{?ES?1t{S&vfsTTt+gywrNPi$`EPqZdd@lU6T7gcl z*kvipSV={mc!?HVnqB&pzM#i)EVTwSi-68-$|%_Ly_!DZOuY4qt>lNQR#DGs;XEs#K9co`UqGH z%ww!og=FHa1aD>(>kNL!@LEtozoR__`+~6q{-NG@-@#2ijSJn{ z>+g$i*z7-b)%?!;%jLb&tuRRMcvPh~y4YYa-1+YxpXmXgzkS(l3|uNB?^wMR$sc}E zOsRwBib~p??^jKVdF%1vh}x3xwanQ7*jwcSQL&q&>M+t)f(24e*^d^_a47nna@`?3 zMD1HHgrdVZml4KmRN4#o^b**UE)n&X1b4-6cRX{&xt=bZNEWA4Rz&+A7ON`>jk_+< z^=5TM)14Hdh#=G-(A}RapT8#^S~*9^JXBV8U<7_f+gkHr~?C|D#n551p%T3OqQIhl!k{ZRB)y?}sE>5Sf zw&aGjjxTX2#X==L?FES!c2RhmPXrhqbJ@$-z6#B}td?QU7wu zYc@=ib~JR~$xwsf4WX%c+8De2%j*6#r6BIey-UOQya*ixQ(ipqaT4(w-F|^I%B|A) z2T8wx(bUg~jt>&ey6|rN#8SUZ_AeTD_i=^peSO#H6<^x2_9zpA!{S1JACRwWQMjB+ zOV6C*eMdF_9`2fyhN`%I#u`2?c1!maL>x=yU_OzLoA@L)1Z;JeoROMoHN#D1!?-9C zl>e2Jt@`FQP37R5rr=n2f*3AviezxAEI*CDySUV)HAG*O{rP;%_2^RJS>o?y10f~a zw@V3igEz!Bi08s%6C|%p(-J{wdVn8!61rJb1#(O;(2Ro^h;rovfaD5^uZOMf_g$s* z&i?F_E7F!#hPQ?@prDZtzi3A25S>jW3m4He>GtpD#o?;)KDS~)q4{VUv2aTx?-f&& z%#chm74wW)DlidY&Nfvh^W8fbq`ceEh#PasFcL1lL77d7Xlo6o=?Lr(+*8w!?6FfH z8hJxE1jR#)#76L>xn9Ta7o z|ALV_7zWrF7p}T}ipbK_%{t$m$$lN-3!EeAjp8mb6OESO%(V+@Ed}cpTx5*zB(tfo z+7%8W3C&pUvF)TPpmJl^QnNA^0{7i52xjlR%>F)^i}Z^`+$~^-tmiZzr$3OOHQXYV z=fSnsP$EO16E`={oHvg8W@tx0BOl_CH;#jZD9LT$M69Z&)Bvu9N=8n4)Fhk|Z&RJ2 zi*+tW%SRF;ye?LC_w~I}wsj&+-Gy@P@^{v|SZaa8mjdJ{_?)u3*-2K<#2H0utmQ>f zj%m+T-nJ z^7pSVhZa9B5OGjCe|^R!ko&$|I!AdKTR))zCg>9=ggpg8dY?NJT7Bz_ZflrRtHZhS zYmdZVGg(I}_ltO37B(NHW9 z()r?nVgn0;U%ZJZ*9gBKDvLepuCjzvS)142wm zxQym-103%7X}}Un<7DagOLxfW-uvbBh?fK}^qH(WAezB#6zx+BvTgHesw&S92_?iYmoB3mApVrO|2?y?t5PrYbu2nYK(@ zbuP*e-HG3eq^`Xbt)NYSb7SLKtH_x*_no+QKd$ah9w=AFp9T>88WKQx`?&WX{BHny zEbJu{adl6;C`4P-+UB*BQ|+i_W6+kZ9xY}(&PVy$c82N~L#l+#kt))fR@!QE^Mf8A zKCMtj8HVlKWib|ploc7MpwDl;%EbWg5#PoPxzS0C!=`FC=a)^eLwuK4Z?G{&&zH65 zDh@MEB_M4|Pc}4&Ye1$&6wVe2PBZ|tsC)vpCgf*(gVi=FdMq;}@GC6|Z{>1rhiPcx z*7#vEo~3hgx4o3i8n1Z`gu-O^QS7R`85nlo-J zMm_FwvvYK;zPI^d6iZ?-uTykvKMRphJ)*dK+lp&EwkXqIEk)l%ee)E${>#d& z0S8%-?KQIlZm_)4A$|R=sg3d3s`GA@y?9x5$kLNs_i(uVrREt7xKR*`I7wo@r?;}# z=s?~X#~Au(91?^4K(*Lwl2O)b&2vl`ZMO_oEmHF|WbtZz6xL`;OK5HjUGYEgZznV` zYzH>BZuwnI-+Q7P$t6v}sKLnutjy=^PTdZ?%)y6vvXm^xK+{gBoQW%`fs5VjC&ye- z;4R;pOGgq1jMdv9t!g31W&3cQx!d;2kUsaEnj4KjqTUu!`Cu*nX(o-J`fWd~v8vHZ z;D-#2*RMTZc_GwP@TB9F8X~k}GYhWaN`C>qj5Kb&Nxs~q!iPO7qiO!;K91xXyfqZT zi&=~Z2kpPi!@@XEOE9AMK=8ZyHT2ic>MlBHyquNrG5;?AvH&I&rsLI+KGL_D2L{ z;)q1PrWl3273;kl&Me_j*?ckTjh%0P=~O^BJ8lh3*RQ`>`wPx4xm_mI+c^2m@F-|O z(9}3YZOAB6yFF4r+g0`@xtfM|)KVGv*%C+!oB8F}A|BJL&>aiK!MZ7r=K2{m#vB33 zF|8Mi<(EDVg`;t)HB0r$Yj}%Ky}TJICvhFO8S6b}07Gy)c!)eTycMA7rRoSNQNXwcSYiH2!S?8w(fkBB_U*{EhM4Mypn}CCGD$fYK1w6 z8cl9w(z-jthT@Ig2Z&nNt-kuF4<0H}pM<~9>QaCM+1-M%C_bbAzV1FM)gLo?bwr4M zO9=6J^Zb3H&9iA|=AHW7BvHw){EyN)+*o#eIL-wy+j^UoN`I@4AkSF7t^toCR)c7$ zVv)OEkxGb0JoN>6&3U=0(NxLi7F@j`St{}9ZpunRH9&(R3WxJUPj#nEtDsb7BII*q zV)MQ0siBc3pSgAr*)zCt@I<;-K5&|fC(iUx$vC}87irxnwE-php`_w=#siS?wzT`6 z%y-XqHM+1z3xGyh?hMV+>fQlJpM2L`|7H5zCAA^_T%Xdn@@Q)4#9dr8Ev>0|=W8{$N5%u91D_b-deHrOuyNA_I;ZoMMoN(q=LRHlw?RR@;;vQ z`VHr}H}@}**pQEu(yiWaR!iif$7Q}cWRsY_-V|GaaGJb86h&L(DTPH&+ea}32i}jh z{fc4z)Sf=M%q*6!>ioMN_~7eZJ-$?s5{an>bOA0L-`{01$UT2nN>fxkbvuRcW5a%e zk>mmF#8RSIm}>^+qF9%|ke<{c|5db{a-tWjOc3>b`Gms%#D`EVr4q%0#d05WhxWw*i~n5XWfYYHJ>}{%q>OH%}fZkZRt?eJ*XG&V-#Ljo0xA@49=se{I2S zycdAaj9nalXp$`Slp}NPN!6i&l-D>FoelAfg5$f{A;Q7k`z7*Q3A6SeVH%KEy3uNv zB)RhkbnKMnbuEgQu2Jh1;wg?e5jjWS%V&{P;<;u_+9mjg^)sFq{RSPDRv9^6n9S~! zi(+iJMVGiwEJbl&L^~}A!s%^5B`e)+sj=};&7qYXTEJ*hzBA@LZl!rXchZ?(Hsf7t zl*n_9FL^@qb%fA`R8n}gbl;@Z!FF~*@)pSX>V)fT%6C=$jz+~_Mw;qT1bmm;nt$h0 z&i602Rev}X@*0V6nhluTrNg-p$TsA;@CtDAt+-CLUU)%HUuQDnsw~Hb!(2dlhVm96 zU8Gh!*4E|T=*zw8nk2m;7~&Ew($zH8=if)_Fm${PsP{t4SZ3$K3_lmASwbe|fK6P4 z3*jgb=Ny$ujC@Pml{pmg=sgRQWF}smJhm^nrf0?sL?Sg~NjBI6k!oCnB@TO6wMuax z5h6#TjQxVzIZn>mq)W^28Q^{=S#wmBPzy~Ic&m~sHs3G{gL6$zOSchNm|t-&0GwDR zPS>?=D~cfEFe_q;lHlZM$T3(4R-4gWvDXV^<+x_O?I#{%!_7O~CH#!X0Y*N?-dLc) zJm)pY73(xAeP^v;xeB`{uy@dQTt=HxmwZPuQv5@60u5e`*`jYGL2HDX%vHk&G2Fg|1g(7Sn1YzCPFJ9(YNaNqeU&UZj|L&|+K}hk z;OK5V37u2&(tz&;>-6$t@00&c(eihmrWJ*$S3aNiRUIR+h&pM8WIyw@`v)`b=Hpyv z!Kw(ei-^7=Gr;yzGSWbzRm?gt_-Z9M5&l4yKo&GBk?8}bfH1c<2a%sYi)aEQ4`dLxe92pwDklS)fDISpNi+yjA zuB*titViUnv3o>Sol4&DrN%W@+LY&K#oykE3)Bmh3vC?Xh}?Jr}C%a#Qs_ij;Np{)jQQ_>U9 zZpb?T+m>E#6uQr8sf(tF+e3U`44Etwnyu;1Q88H1cQL*G8Z}W710`1O0q;GeYUN6B zmy`+H;sj8{33uXhYGgm^zA<+(&QN1c`s^Zjo;pY0$|ZY9sV&X!s?lA(B|Uc1^|m&X zdU3{Nx-rYUId{SH@Y`w^b4x^6+7Fb5DjG*CRzJP1+ut-$QTPiOX$c=Vx+f7imz0g$s3(>paKB;aeEkQ67peC`|v@E6xCs*E%E>nXgm=9N2I^-|sLKK37*%5o3&+(QC zq2!lo`mtoL1oe;%o8C z$~PotX0ox9a`TuYd=pfRzvX0kc?3!aiyh3o`z$dfuPTL;6eMJpS;)fcDMWsc`E4l8 z^CfL-NFuXH8~&QA!P$8HH6hrd@|&*EeU}$?`^6kX{h!?DZ$FS;`F4`@yx8%*_D04N z=x;B=Ps*VG%F2&vyv|;Dw=ao9sMWbMT{N_RK_O`!-3iJ>@zWLMy)T|$|UQ_?ITv0+()!0T-oXCz3(anwr!psDiq z;`%lIB&8&jzR~WuM6#fOVETT4vo)~J2G_-;O!6hkDjeK1U+966csH^bZfPn-sS%Y) z`S#9)0Hf2Km>7JPT9UN1x5P+kZfWvCiAZjnB?L-PeWe(-tn@|Hyv`~UPlG;yais9b za=u@?Evq3rJcS?&(r85lh z%mV#2_Ikz(+8IWG)m(8Fv9H<;{_abD#i44Z3#&8>@YE6>oC(-d-;b>~pwDD2gTusR zbwA&tY>fvKafHqjMiK|aE!hNSEDox9N%za!_0e;-EzzdYuhucr&EPxWfUe0`0dN`W z-;9sn;1?EF_e*k855L`|fg?=)M0#>~V-gm&@65Yv@${qn%7_bxJLj)z2YDMyY)^bs z$8>4#BozNDkp4|$iiiI5tLH8r1;t}ZL+Zdzv9sLgWnM$H zOT+M{jhNyvV9+QzO{lmnMF{bl@D$gR9&k@)V7 zpH2FrM?6kjQZ;x3?OHBi$k#}jS2NKDp64oVjvJ<5sAhkURp9;RjG^MF0ewfEFyvBi zlqZKIqrf$hAqg{FTxnieaY}j;`6>r{l`F)f6xC#Lm`R_(5(ZK1v`^3QobjSX)ZcmP zi}O6cuxG~j2{f6ZOLAS3JEOonIder&h*G-Lbd3muo}7c0Lou8Ee0#G>rUDBsy7b6b z>(#{Us@1GxPJ7EfB5`9y)ohJH1Q%|c!Z6J>)KZW%=?e-Ww;=$#wX{Rd6}GC$Zu7a zZ$2nY-rRlq$zxq~@ZPC%cghK9JFlEK!Xdn9{J+L!>chCa9n*;`uW3m0j7JS!Q$V98 zM+?9RXf&q4&VmN*mu5a-|3rJG+nT(`Zggv6+(}Qky_)e^M&m8M7Y^C}M6aejmw^_{ z&XyD*k2w2y$XuZz1P(FXR}mjtUJ^=0>LALI$AH*F%K9(gf<9tEy=Q=`+c~ea&W}<> zGv)7OF4NI-%0S-7$qgBeX#i7$<>qgLRHT#0_k2wyXp)IHgM1TPod&BWUT$Q+nt7XF zCQk}usF5_p5l}~`*PwU{SWVcYXo~YC53Zj>uoEdhk^ctuY`1$%t?58R-M$g!+e?@k z?=<0$mUXTm{jGiU>%>>?C1{e4=OVZ@-sd!nK!Ct01T_E!=# zR8|ke%u?67RP3=R^wF+#UX$hSR!=vUD_5N-O9MH4^@-))WMXau!^l}noQdlB3FQbIQ!zgW!ya8G=!V%= zjFwe5RSD1?u-Xe1udH3VXEP*Zt&w_(`L)|?-tzCaFMNM@d2(+nFV*ejhM?C-kSA8+ z$a^?H*~y)%lWhGf!v9m_aO>JZ-MAHCcY)G4m=AjolSr?7f2yvDhd-9li%)z$ZaT1? zhxWOHu@4OSkqyHZjuP=3#Be{*7q@Y%?-bl&^zp3(9GG4vh_@NbSKy;n4`lH-EPxCpNU z8$uHS(4c(#Kp-wIa02wTC@P0+z76?Nn92Q`4CZHM5BXmgQ%@|Pr2ux^#3FI-vLcmO z8X#IuE$Y3o4f^$ zM5DJe@AHRXe$8RVB`It()VF*(k$11r-h1BO#^$kB`a@Cg83CJ)BI_QPi)QbvlIJJu z?6!&XowjxxQXEB=ZNR38X7)?$} zA^1lL_W2^E3AH4|n3x9bJUszlu$Xd1a}hnyp@WS%S9s|CB!j;s&tdjmXGRoVyzIfY zoEj6ws!OTJ2TbN$Rjx+=6zswuDMj>fL^d;Ym){Fjubz(;s zEW^(@sNl|PXfx;N6@;gRu9Ht_Lu~%r0kd$!0K!ZzYyYA+h$pnpmnevVn5`g#o{V5IvYp7Zz1;FGoP~p3JKtO3YBnu@ z!fJE6)16y%PZ_bS{G-04^2#zWH9RugteX`KT9xh@xH5c%DWz(dAFMt zUmC*knP=UqQ(rXL+u6~e7v^R6+N@tyC&_gS;Mg~8*fR*o^l;t%IC2n>`shK*uN`d@ znf#rtydkm_-&_Clxm4KUr;`p+Ixn3`?yM|}DvHiu^X`ZoFeDG4TMp~hFv_|t2PO`> z^!!}SMK~2HBiUv1C>D)RK13{eiRT=2gI3IF?i1+IHBsee+{#HTElyUBBtD-J8=bB4Oz>~EDX`+<_S^Rw4R6)tQetzcs^lA9u zZ#nM*<6vYP^Aey}MFiaqc}f?qtBW(pl|{Pr-r0||%#AX?CHpWgkF$4s`KD_*bk(1l zoYZ}iL~(H0wJ_of$F+dLiAZ`^!#ijG%do_W|;bjawl38iU$%$u8Db39Ts1 zOGEnSqu4|;bh^+TqIAkW(0XBP&xX3*kEWGy%U^yGZVI_kDrqL(&}m5E3U<2qv8FW_ zXPQ=nKyVJ6O(?Ay6Ls*mBayIJ43j_5K4-?nXbFm zc{7x`PSzvCz&#Pb^L;Sdxa>xZhAm#k-t> zH<;VGZ!T(ib0I&V#m{9rqN!iC=p>oub=zC`F=_05#OEan+kOXS;s&^;fLVIyU5#pK zMJbw0uWm6Owby`GLEWPJSGPX#@?}({v8^Rd5yX6#kR*nvy4h>&lgyFXW1k_yFE>)yR1Zv? zU8vF$PZ^QY@R%m%h!V(8drmvXr>4-FONnn+390YPINaCQw|o3fc_dhPpOEg2f;)$oduc?ZVXpI= zPYaB!=Fb55G>9d^;W_D=&v@tVRozn(O3k0)f+zzu*9AFU6y()n_9U<}>0$a3F3@;3Yqpls{~pV|kCX86Q5Wlp@5AXmHM>twZ0dG^ zyqBbx{=HWJ)&H95Z(yXnXzQn;w^SSZm_brhkZniCF-7GnHqabq)p)R-LG^JiaPBxjfi{!k8V{7#5)^?A(*=4Z>fhwVA$?@@L>LXtQY`iTXpxfuf_{BUS zTw_W#pUggW*M8XH{@{0@5;p--EAXk=^Zn-iq11#qyz32s$?W_<>l-y_w$zMDb1dOC zNA@q`B;Y`Hdawh6TQ-`;n`kO87`cVgv1k_qhD;=nX1tX3h6j%ctVHr(iViZ7fBR@m z_LX{#tFY?V4D6aFclq40& zarmkz6&1Q_iEr2S$kXwph0b)UDORk>uN!u}N|?I$>Q0IbK?1omY#n*y9@YJCVxUWna^t*lv!N8S_+@f(n$9%=v9>Kbo!4J%J3reO z+*(9d`c7(5`R`QkNXH6CpK*<==8HC7=30<_5{O{$r0x{_8m$sfi>I{DA`eo#JjdHA z5Q@Jk6$2*UrVV6lZOP2fe?trRr}OYj6eqHjb3W@+Vdgk^fg6^O+_2xeM`M>n-Ad5x z<0SD-Wv_sB!wmE)oj&r``E$DC491t-LRsp~CMKdkROTo5%U;iZd|w#DD#uWO&JKXNa|6f0{VIev7e(>z~<=Sx%l3gD;)*v~yEanCw z<$`BEWS*UwSzzDypJ+${Ndz*1c=B&AIEL)ZYY~hPvfd2DbMPhe_5LDe-wSel%0U`` zm!bfU=+GN!43o80dp(3rk|im`-){LxqBk&GO%2@yY+t}$Z4*Vxe*quUA2!5$jhq#+ z!+9+#4G93kJDJe|%n!`!m>&=q^{;BJ+!HhlyuUyrCEH2<rgS$tvIap+ zTPnE*u87V9r(L}7PZ%v&MsbN5Yj*T{L04O+xTB(o_JNp36nU0p41M+!GVDdkFWery z7FlZYiUgXL@PfI4jpuJbx3f3)I5>a580qg>Xg488hz(AunQP5 zSbxxCB$A@sx5D*h-7y(hu%uGm({8r?;c>x*a2zp>+%B29xLFyj)E;uXcZ2%xv|%6%2M+h8@w?RjmU@EuUP-3G-cvLV9(1fr1Z<}&%y2nino zF6T}Svi14B9g}kR?lDLFu_tp(xy~GulSLzoEalPHhcsrj)pVe~tRN*trs+|jq`gjQ@o^fSI?Y#6&0x<9Y|}NJz|WMuqcTJWj)f}Kk>b(<$s7an zvFXqy3nYrp$@a_wVj)hAHK0u>k!i9MvHLZlD?q{zN85yms(AV zykjg0C^Hj3uSq1HPY$KO&M>wts20b=(H6W})>I!=-b@ojztI{Uf014>U#A86;kGvC z@eb`Ewe6hNWr`KKAx;0DY|SqE89*C>UWAsUa9V0~GCuSB$dM0nA;FJY*B=r0<@gd= zb#ra$QCu}@JNaKj+7}m#7cnl}`2=KOY{sX+J}=q-EYqSjr*>wa)x3Qj@Hxml zNHa=LxtF9LvsB)Yaoq;R_3v?4y$?6dV9h-hL(RJfh% znkk7zV>YkzGx|)#1Pz+{LI}T;WBI<(EA}&0^US%OOvM4*WDGU@d(DZYwbEkWd_lK; zy>1kMne)3@?XEga*y4Y=BWW2MSo&di8oPW`D~6eBHaHOne^eoAPvUl=T~Wk0PT!Z_ zo-Z$5xRI%#dqEZtYB+bGO0N~^>`%d=IVxpj);**vom@@XRbbq}T$XPcLUgTDOzA6# zH|&gBDAoCi#j3kHZZL99-4^tV5x`OK_N$V+8m8irkUj2B7Bg0bJGmY&+1}iaH!#+J z?TW7$0(Fu5C*TExF)NA-ey`EHAG~V8X5R@Yy=7D!i@GhE;O_3y1h?Ss?iM7t2DjiA z+}$M*BzPdWyE_DTcXw&J`*QAkcdvEE9pk;d*ZI?A>X%sebqz&t+VX#TDXAk8i<0g7pa59;B%EL?; zClh(r82Y7^ly5IOYR$dU(Gn+3Y7jse(N!fhe?7(;XyPFmTsZtx+W zS)%!$?Xg*GU4Hon@vp5yM&tLFJCQrL?rT3Tu?@GHwq9=IMZgThW|l(?>+WY0ivMiRfk9*_iyp7W(k^T*raZSNM_CPAFW7IGUYcxQ3Q5$CPW#a&_T zVhVDGc~SQ14}LR4VC@raAh;Q>2@YOJr7Pkt%F{j_B=a~`K9xb!^)z8^%PHiHd<8W8F{2 zvwqSit+{>Q4ZK$YPw_6p-}5$HMh=s#WOc)J-bJ-Dm7q!A#wQ~*M?X+k-oo|rUzNby zhRTgmgUCf`oYg;V@M8wX3;REA_F^_i`#O_nZkdZp9uI+q$@=kxU@0jkjsy-4nTrDB z=>=|}j7+$2jw5cU&$5LC!--EZv5i} z8Q0_m%yJ$9Ti#457aT=g?;4wQ2vE! z?Rd;AkqZaQo?KR(ik<#^_~2wttG`_>3|qE0L-9E7iIVQa;VX0>QZyu7kLLemeKWiX2OX z)bI4yAPS9f%WWRa>~_(y_5|~Y3f#g!s6o+dqyx+}lXT>~@))MJ!GQE(G2ph)S$L<> z&cv1R&iWUM)o;~BHY%$Xqlr)o^+eIHXxQ=R+U*kh13u+xho^4t^}}R)^Xk>)T=owU zkExwZ2Ym^he>}1Sj*n?CA>r@;mO~V`Xhpw?DEj`>45GsN1`_W}6*ZG0<<)I(h8Kyo zcUrc(RBUz_CUN`DoJ#j3Lni1wTVy)}bu4Zk)5zSz&>sO9oUJ@HqH#9n1Z%-W@5P+= zcxAmaDOD(7i$JyXMic_$=9tES(yVC3$lO5YgI+!}3H2YS&iUP9bbzk2Ek^2;CP;u*UJiL&LvLy7nFLEL@koYD=_F7I z#Uq3ATTAwoC9HAiS8p>t8CTJCpJ_m|82gh`a&Dc|Px7I^FyBDClA6$BRO!~}oVq^+ zBd~xpQfA0u-0JOtZ*c8g`*AZJgXqbWWpyKfzDtT6Cc#QCJ@0Hdpx%&9Usc{kH+8J) zBitV~6wLwiVX)5hZSdiMGTSWp=@R<#W2tS2!noNNd7R6#reSG>PZkBS3~c*)lNc4O zZc1%?OdIp2vzqfJi}x|M$`OqEhY=h+^=N{uwbWcBJF~v}>DIy1_j0R^T|JE8<5(X@ z$g{y8;lC<~?)cd5imn8IJ4({wzoN%~G%@J(go-C?f0PMlAXS?yI@Sn?fjB`2irNgv zID*q7u#x$&2H2`!bv_g*=m>Vf)a(~lh$nt7&689W};(jpx&&Dvs@7UXOKk*$M5NaD<~ln`~PDw*6CL z{Ke3XucM=vU)?g!?(d(RO3tIen}pZg#q%9Vv)>~zq~_rHFZvKjjvqKCGU%`Wo^sb%h^flnLXXTOlM0y9Dds z7zlQuD7QiZ>8jL&&T$}@5J@((SLAvqIaqub^cR2N{r6xBLNO36KQ%@r26AJ0QP4Zr zpq$^5uw^u&a~2JQV%7-+ma>eMCDbtJZ<9kj8+sNjTml(ctq?*dWbto_DK*K&y&I92 z?+9hTB`}JAQ*A#s_C+9RU34o&0Z^$0=hQ1lP@bfB7f9)f?$)cw(PHK2)o-+4%vD3v%41eIMqBT+W#eG5DGRZf7P6;=GK_L_c z{vXt~Ew!PffTAgZD&v_uM`89j6D*DvRittoM`eQ09;nOb94uxXzCT-_s%ZzQD<@A9r^0BnF8~`NN0!mkyNU$S5QC66sMCiT| zCK!VKpS1bCBC~p>id%^9eQ#=nI!z!=CU$}>=F+R!L%t%U68G>r8%X}pvq#w67 zW5~4p#3Pt{@DlQBUK+-mJmqp16Yatp?N8=ywAy{2-`3DxsVkyNme}C!XTkTaHsFn4 zz|9v|y{W%-rCxav`pM-*;zZ+2{vq=);tULrVAX8UiL>BWEEA#16OyBsh3EMVZ)0Oe zq9$z?$>vO6wx!Xz9Y8F$-@}F`vrHiCD*Or|;1a**MSI=~kZ?|C<|k_y8cd;7jDD9Rrm33{=gL~@yS*qW>My- z+DluR)*2BKeBJh>sF2N#Trw{hVI$3!cBQuc&Z~I?xTGF3hg;sy{aBQqpo(ExAGc#& ztvwkflbow}Q0A}YNC>FW$8-(ak1H9ZtGb!H;b(Ldk{p=n$t^68u3FX>tcy@$;lFDK z^e9d-6FvWAA5{r2BKWDelu>O({4>yfm&nt;$uBUamvf>*nxQk~uwQ?JbfeH@7}=CO zP@3i&h6#hi4N(4rT~MMHAN-9TD>_Isgs{3M6x%z_Wb;lLso;L)F;cANJvZ*5hKSK)+0^2U7VIT5n{M}l3V$Tw_@RmqA zU3WN$mB0rb+gPY5F=N1+U?+x&0+G_dYz>dzn0nDzu@A|$uumsp;Kvgd#&2!_4?zE0 zRynJ+v`=5V7<<2hVgH^##RcaQ92V|T%U9aww9o|NjHv`Ww`u7!CR+^JG+!EZeOy78 z&=eAh*}8WH_ipBtp|EuKVw34s3p8I47}XG^kttL|B3!s+q%`e*G1G_o#grdYMbhvG zg+3&hshHI_K41vRQYR@k&wSh|IE;j$b`D|qpmt@E5>uKG*z6&4jTruY&f3zL6no+ksi!tK`E z#x^U>Gs&jDYUh%>im}H$=3!7kHoAz=ka& z!@?@`JFeq5CH?D%v4ktFxs)UAj0)jU?3=$TmNz@SF3*j|yq^Cy(|A3CDegM(zeC9V zr`w`N7=iHQ?YE4cP-YCXWSfwwXI@BzJ{9J^AE&uNu(hdVKe)sgCOV$!{rhO zldfJHazM?nwL;NxRQasugGHAh71enuo}kLe7bZ(N0+lGG!nJsL;@tY4lKHMoeWa9iZ2Xy(R>^D(7r zlua)>l!eenH%y9YLhEN5iFR*PuFOW&japl-6rK^z2cs^cx{4s`>#(^y!IDw7s0!+# z1+e>RNPRDhCqelg_MtH4@|-4`ygau#0fn6UN{2xC3dg4yG2a>Lvgw#0Edsun_3kRm z@uWk~xZibM%U?;yKkrjiJ&$u7hsSyFu6R9^FWibcqMSb5p}pmmPil?m7i$5)%~-kr zCZ3`SeAS+h=UtxtkJ#y7H*ZB#Ty6EVLEPuC<@zSdOBdFc)@5)!bnmi+1^vS50z%aLtx+Mg9h~Wi;^m5lAi zHON|XCvD8z_fS8C<6v4!%$v(e`9%$=WC-wA1VyjhyyzyWk%UgiFHEsFpc#f@9VQS! z&qj2Og(o9Lve8dzClL|xjoPT;rnaViP6RBTg_TjE(CG2j;MMZWG^NX3FwJ?unutndtetK)E00^>;63z@)yN*~Wm%Yd zRAWE9@sG-$3N&r`UlZZB%g8*_4h-eJ5dr z!A~HgVoUh2?&cwxd;GqP3ktfm{axT@xZsb|Q!Ilg3?p1kHkkwyC&@1&(wP}dQ8c%S z??yRivD_b^j+**ol1_fhx-O_YCKo7(#WTU|hRuqFV*V|oL7;tDAC0OJisl3lp z&=B888dsK~i{m4q@f=}7<8t$B1t9=GCfVX>fL zV=K=N+3TgD8WlBT0Qv`K>(N-8-FqXw5S_PK1B1c)+4J#D3-n@413tKb<4x2MyYKO%%2bIif1q)jQ`BqZL zGzq02HM(}n(R(DtmHRUWZ8Lp;@#<-e*~Bs-@2A{kwJt0f$HM@`BwLOQMefp*{0oeF zz6{sUa%1V=#NViumaRmG;0y8mnrT!6>?grLmcJ{~Nrd2}hcg9QR1Cx77koh)o5QcKcQ27mh4xm#3zC)M2Y*tM%9%kVaW#~+7Q}rd z^l*rlI@6n&hVpY^1TrC;zXypTk+W&jn6!ZvwlM3gW#at`&6bXi`d`-tJTBIS_U2b4 zeE1K(3%~4bw*~Me{kQ!Y%r`l-w|_`vuIWFdta&_vZw=Y2bb99+9vqMmQ(~wfTIXg- zmV5v$&U=Pt9H|a5Q!i)@K=j+^I?o zsbj|z2myPV!z3}Olo}N=$Vy2}0K=G+rEVcYES8{`X^JB=L0ka@A9^G6>tMN}6jT@? z`1M0}0S#%7@%*5u7&tkW7{o&AIfMx{k#OIVwG9PXb*`-?W8P;++#698mOV*@cuV{3y?I-)Fy% zOfxmCb6j^P8to=h)Z2el38>~XN$dLOH3z~NN5s)!EAOv7Q=CroAdYjh;EEYPZaK={ z#hNHgeW#zo>HINZzrYHgm8`{;@RGRi!!^T4SwV&#G;8~)rRU#kH%&Mqx?6?WD&drA1;F~ODf&|;Lk%+y!vMo1DFjtqJCe_B zm3v$)J3kh*b&6u*CaWl#tLl}Cf%AfP_@m4^jWPKr8Xr? zvV?T}AeK5WKy7&4izbLJW)B0Ac`t1#eG*e*7f^YW>zAlu`1y@=>HF=_U2g-3MDO*bS>_SpmvrpCW?YwG3wTysAv;9OCZFy~lBYHeFQ% ztcQuWz_VVjJS_PtGC|shR4M#VaSi=aW+~ZeR+~8uKgWE9U8`&KJ}1RY?i5KRyXo6! zSC#0@f;iJ2j7xxW@34}skJqY##C3y4oa^NcqrG|cr^6K`r0^wwadw0^Gt_ZBL~C)D zZ>bOK_uB%xejj%DANg!VC>e13+`%JnM62L-*q`qDuYM;X(eHY=*&mx-wrkw9Ux;5i zEot{7=RCiu-e?EjL-fu5=2+G(!tzF2HbpMJ#Q!zO{nteI-#G}J%Ipp&2L~JNQ-i4W z(Y3^vU8>aCNlH2)NO`B)vg|zfJzalxpuxnNoVq+p&zLMji2!`fg!J)E3NMo4g02Q) zkMHrK(J?CW9Iikd1$9#vua!Aq4Am&$yT5cg^SMCDL zcGF*!cnQ4e#Y-aOnKc#EEYeg1%0Jv-q}zp9T_;+jE~nZLM+kpO|2 z{(&iMM7Wt>i$YK|zUO@h2S^cRc?kf!R=mOA`ve@pKi4id|5n(> zfA|eFyC4^urv8hw{%dmo?;LF1QoR|g$#gZZJ9G|u`!08N1bYX*XCo;HvU~FFy8QEy z9RW}5_hS?NIMzgw2Lte?ZtyK4sZm|1CoLD1*IJ;}#Apd6S4G)%`b>U&YXU}5+j`fw zCBCah=ky-Sepk4&CyW=F!AS5p4E!OLKUZ}bs;2lPW0ixayn7g5Zui@cnl+0-6o6h@ zi>tsCS%QXuQ>ekPv^;kx!{W$uFBS~kHOf)Qd7RI!GUKb`{r-bQ9Z;W}?(y&9U^~4pmJG*r> z`!k+UWE8to#gc2kTHArrxmQkqoPJ~|SE12O7}~i7me3q0<&0qj@i$KC+x`&pW~Cs7 zvuGr)@-Nt@(Qf@yEr+9=-e_1-H{M?Bp1w0I+vZ97Fb3CkI2;y*8D1d$XPTj~0|!qKt|C)_72OmLu1<2Bcl`LMnCA$c*j zU5PiZ?%DUohyHH z{2?RX53s0=a?N^ty72EA0`W_0rb*3mqnCxblztFip_N{d`NB(gdP4KDhuHV3OFFK3 zP#fr5U(LRo@Uk7jkN0_p16B7YUDI9-ML3s9w#(iw#NZxZ_)5l-K6SPIgK%^hj)Z`r zD4sFFmO0qZKX4U3m5+27dM)Ttd z9Q9QzZnERUK1ee`ORcFPl#!M;$@)pRDG`Mvo}Un%cast&@dp<3TcRo?*mW5BaURPt zptN_;D0{y+Txipzdx((N+Sm6%a{m&(E}=bG?as%gWlA-wzNi4^765uxBmx8~bR38)F)lJaRNon#*n0tneIit=O>{s|{x#-^P zgObpJ>3Y}6yp{=$2ssu&$Cg&RH1>Lr!i#1+>C32@5vY?vMbz z62OboLMLIe6rNUAVrio5xEG!F{+wGm74vzUQyJf1$ph#H1{OFqggglz3NcEW zk|Va3xwHPaiuSpNjRijU$Cj-N9SiN=`X-P}XAxeN@5yH$zt403a^`BRDj^@pG_`K8 zYFIp06`oy+9GbV9J8toV`U$Eh)+DXDpzf|pnxd7qtXt?G)UAQU$~*r zivqy7thuq)F^i6qrYDmsgRh-iRmK z#yn8@^9TPiUM(f-jPSJr-9kl}z4UVGDQ-2qum*2w4#{E;68_C3kx$-* zVB}|MqI^O8Gbd#xr{;Ng%<`{a!VRaDElWa4hACEPBP4lTe5&k&Dn+6K>^xUIByUyomcVV9*H(bQ4Teb|wvt z5Sd#jR17UgDe*QzD>WS}IExy(@f6S5Iy<~WFAzss*sW6`ZsH$l|OOd=`fk$zt#~HIk&@kA6)0; zkBG$Uv4-UmPC1p^r5FhfQYIW*ECN-Cs{aV<)|(d?l-}D~)l9 zM?YO$XKU|~xyAQY?Ajv+`T6mR6oXmFw;i9pLCW+oI;S>^=Q<*#e#o;S3wIpu0!@3g zAE@!hq;+ra!ERC2b{cZ~c>3r77}H~F$P1yjlL*&DI(?u1ZW{fpU z!XSc^7lZIU#2tpgh!l!KsZ(plA{I)@+C}e~+^K<|S1wvVrUjo_k`bs)U=I-ZQUA!t z7X{}8wyRTC@3|Qb0DLPc>1B2XIJMbFDvrDX1bg@mP4@lK?s61~)qi3syz<`M(i984 zbW7D5uwp5t$46}4W{OYOs-ieJYA?=!SkSd#)$?G= zF-&kECnWbq6BZu7ys$zGyipVp*=y*K7%rzb*sA;4$M~+6bZOxay)1Y~l_1J@E0liD z4(>4QoeWz3iRLtfc9CE2%#Xs`>P0qJKa=I=onYj;*{zjI4Fq&VX_aBd3|F-}3AGP>iB@qOlwtTu4 z7A8vYK@W$j*rg(K%;-mw@_)lIjb<2^?TvIRt^cUSm${(Pw(}*Y zALm!0gzZ=j0;}v}6l1pBhcLVy5d#e&8uR!u+l7K>Es_K4ioJs$EGb!qgth z#P$4~uacA6OJ}WxLJzIj`g1I3Ho%^iot#;}`I76HXuqRMp}ub>*NPI-GjEbzew=KR zYu9^z{%2gjb`@Dpq!M6)9!4z8Nu5_uL&?rK&S&=ess-BkBwx3<>8PI`Nc-0K5@sG$ zlNOyUg^gva8atG^YV?SSF&0*W6&i*}lk-?5QSu7+ zuI`nG&i%}QA<5WE_|jrHFefbXHw1C~IdZI1u6 znL{hS)pr^2xHyCK@FW@#kA94)6y;42o{fNOD_md{v+g8uSZ-r@6$r{t`cd2OC!Aq) ziz%F{i(x1~xs4k_*S!cVG*3U3#UvJvNv2SB?iyNr(e$Nn3i;wezID4Dz)K)>t(jgl zFEme2G}#B4+Gy)o$t?yN>|PO@O|~d&H7t~$T%4S&1E={Ml?o(7)*=EzJaHXIx!=`5BabdFi)tc;_F`wRz-r?dwIi(AAWiHbnu@X%CQ~C6eXgc*Vyo(xJe4 z+|5GS63)o(4{7rW##ifRj)1EygM`0>CKNL;K=~6Q2@^huwqSWM;T$Wp_ol^-TX`_o ze{oT9LEP3sr*;GI^CpWt0?lOb5LthV6C@er&?uBvD9oL+iCZNY95XI<%3vD9kWI<+Iqo;X;DZ}Fx-d~>pt#xE!&XBwZH zye(Bb2I(wtl-%dtl!*!THoFLBL!mGm@ZVCN7oI(bOTk3B0cdd zF|o|F*YLrGK0d9Am_s-Y;H7>nI~wI*LC9Xv({GRczTh$|ZPZ#%(h?S;kI zRrEAuKddfip;0B-mmV7>6tEu_AyAsITn{KrzC$j*QvK#nr7NaBL?JFqMqmqQ+wpfg z>0#|oAF_23$K(M}(xqd#6b0hY=ilcGntjJa=lw(m4_=8EU#!MYGUuFV6@(h}@ob6>bOH05K*_&~EzbU3N8vYg;Wyk{^*Fa3CFl~@Mh zbxLNyYU>4u*HpKX&~R2FA7j`3;fj7-QR?bi+(VOB0@mYhOG7MwiT7wn%JWkjvEq=~ z>herVs)!NMvsOR8OC-yEA5M@h8OG_ua71`+>8sGW!GoPnKm$dih zc8G}1WO`Sp{&xteEL(s>|K#TX1El{A6%?2_2ptN^e~zHPfCiqSyqXl~0y~O-ea6#_ z^{E?p#j@-hFySlY7~2VRz*5OqZkMAJl1yE%zH6Fvra`*ew%Tp|G=o2m0h zMqV5)a6}g8zB+C82B|j`_H~Hz{kIQ{W4K_NyfgeI{N>P=Cz#QWmfMPIKTOQ14T|tE zd7(!di@WI!pBuJDr1@hhp*!0t9|-NXR7V`OTK)*di8$qZ_1zf3u&J3cDRQh&F-nI0 z9Ebva(`x%6dmkU*5{obI53hzps`VR)LvVF4mVzTk76tjsG6cj3%^*!!{NvU~F|K75 zhf zXua5wg82DW_DAQ4`B&zUH9lq^%8EK~&UkNlbRO(YgTFo)`rY$8@>h~Hww>U(Mk^c^ z_#YmEW&-%MHo#um6g`UY!vexNE3~IsN`Xxy$%`{NY0=gi%g&6Z?TAAmel*c{H+(b7+ZI$Izyyu^db_W}n+icWb+OWftEL_2&~9-7{<&skOvsLw`54eP=;v!5Uv{0& zEx>ULkG>~I6&63c$?sEJ7X9@$8-TeQy7AkGES)#`JU33iesO@(k6&2s`bI&_u?m1}cUueBO<+#sG%`@&nTy71oH&%LIl#VfDR?>oU|(pEM;K7L*HScOquF>Dt$k4S#qIBmti4anLJ(&cwE z*aY5T&s6+ZnCaFg32X~E_8$lwz7GD6A@slV;2Z3=$jP9-`aU;p_&xeS9{jufTHQtMeY3q>VwISp|5rwJ`JM-&ht2Kt>tC!QNSidGM8;ZVb>4m_8ueyx%>f@`eR?iRe z?OYs^uE7g7N!Y0 zLX(l93SHNxJL7>FTGHmhySUesj6P4B?WLTFePB&8`ZSAFu|q7HlpyRXj>S%po62nc zJ>ReT{Pp9)#jit*fF;%)7>;m?a53X;YvaXMEGxi3%-df26$O=IvO!Eqt|r>0k1Ppc z5zaHZy;+W0RjJD5{A?cnDHTvwkdXa?jkE|?P9XSh_HFxX1@FDoDy9d9j`x%@Yl0eY zG$6}lw&esWV36gtT{|pq)opBg+daYA*P$?bih`2@(?#X*U{fHm43+$pTECv5g8}4mhc4*R?@7lZ|a}i1?WUEkSk)miW z7Bq4hQ}0Fo8pI4_d$%_^Y?nC(V_eSkfMGrfCt@atltl2*PXc)N`o(;j(7+zhBMbOF zF=&TqEm|Ag4+YPci-j2%g2nmOdu*=IXs~`yU~n+q@NsHpCyLEc#8fB zgVpPdwo%5Mb-e+L_ETC0J28AQO{}QG4%@;N$CJ>BhyTIHtNC-@MT(9Xos;EzXm3K*!1{rH+5oEkxKKBSAXyIeEGy6(X;v&^xG<9(yq;6?NZLpR4_*E*P zfGXhp$$_RxG-4Y_FTU*0AR-#H{s|eLpYg*8yY`a)HN&&epN``zx4SAWuZOcgm!O{g ziF*|?5r3##J9p`!QpZWORUVJU^Z*@^zjNwlV7lV}EUBSKyz%J(h>AF#mntcF@qkZ) zBp3lEeu|<&@Fxr8y_Buy94kv%7z8wT8PF+kW*98uESC>9Lpo)p3kpc30x3j28qwe? z9-%ldi4f_16^UT_ni#0iLuO8h zNO6n$c%sm{q&#*!O1)d81BG`dQPiyKAy>=xf4l%#(muPDUv&**20Qh%yuF@;{GHHT zeA<=J9fgtDP55mhNgP zlQv)ewo4jaNhkzTIkwJwM|_eP_-*qi8f27I+e1G-v5Lpo1F-WY-%coZ?$As~@`H!X zB`C4dv??|HG;z1P@H`Rd8+Eo*8F1bK=*1YVxIJ>4`WZgq`Dw2temB!e&EJN;2Hqv= z3FmVkZ>|if6>GR{I1p`wTWHO;rFO9FQE2vxZ}MbiEdSR_Np0LFkaV@hkI=EMhW?L1 z)sUa0J4b8uc85>S;N$Cq{>n*-E4=G9o-NwBp&S3s2A3rMu#s=IyKms?U_HJscD2z7<#Wa};}Z)~*uS{&Xj>F{Iw~By_XfGW zUpZj~YZN4UpcwO><*Z+*<{fi-OF~*95%y!;B(a8U+TKl&`&;NBBL=>U@apMt; zTfTUnbfYOV{LA|ao!w0#Ml2M;iUkdT z{qflsN)BWAP6`$BDm$2G70xkAEq-S?8m9>P)yO_-QVUV zTY86cT@nzXgh(|SGZeSA66jb4+%~k6pqx2A%RRlD&?16UqKO)0qL^qm6-A6p*>QD=Hz%Kgs!Bwu5db(G>)!T`?rtR znKjz4*0;Oz8W2|xMUroFq=jY@G>o3FH7AfeAP*M$$np5rx6RngR7Fdhd-+aQ;R*7* zFfU^k4_mfzd7XJiH%_Mkx@AAyt_Bl$G>7Fs5T0j{)e7$lL5d8WT7?Ys^>YjiMP^M< zsmIZQQ(tQDcI)n60s{GOcNe>^6(Gu&R%rh!rAhu&6lD+Cj@{BXZ->g^G*)QQL#>VN@6eyRAO z)&Nmg1km`_u3v2Y*a8zx4tY0b%)T>yB=S?NQd=tcs8(w9i-#h#(iovT(nN?Bh14)% zrrCmM9Y9g1GbG}P4ksR6;Oko6%Lne}&`m-%(85zJgpTuU{Tmhab>T27tt7M^O^998+;lZ(G= z9SA|B8WUJj-T5*)t0MezH-u4ZD3E>7H5wTZu|Yd~w-w;|($ECX^5^&~2vw6oO@aEZ zDm-3uc>g(3@-IOU^?yds7Zmlcc^BZJ(uxR^mRI-rT%n z9@6O>yJWP}s@2$4WYV5FM%dU?H%fYQ9Wa9HOiYO9;&9hs%lGkl@Fw~yTV$?596N$^ zX$Y-Laom8T&zG$DFy!vJOA?F+iD$*SOz(nx6sb4J!@1w^i*F} z9PpmePu`u8pq?*Lj)>ljSk~pkk-J3Lz2AFcBwczZC7!_Ey?lzXR;SG`MKz8730#c; z@9s`%7AJ&&I&TEcjj?>)m3X^#a;6h;M)8w3J6;k#+>7wdGz_+V)cu_<5f%-=Jw}=C z!Zq&Xglc=B$C)6>deBWC#-#9-c*8IJHX+>~(>l(F|AjdO#nl1S=z*6Ucf6ACkj$&R z+kDvS?DQ-56yJW$mBhJhDK_0JCk}tH#D}!{)~VW6m?pP=Vx5rYPEbXWFIK~f4jj>+ zbWP&ZWg1J{8{DIG{bRT#x6iiIO}S~qaHCxLpotA0_VNh9 zZEh2lkUMt_`aH5rX8V9yFz#}i4v=@Mv}a#0cg}EIKY)xoGyLyu9l>P$%6}uVVKtkf&+c!m{*h2c;f8q3$rAHm~^mNf;E0io-R zIP)5+T}BQ(*(GPoNfru(pg+hZuC@j(W@=d2`!}<(MRyds9=~%XiUtLP^jL80$nE&Rp7CZ=bwXp-#JSaCYyjU zzGVr%Yw2NO^bCxv4aVp}%fX9)#$F@Knv`Xj#=pb!U0}CyS5odzUi!F02xkz-=PaEawIrRwjWOI$TJGtewZWq@gXO}~IIPQneYM!Hig zvfOG-u6!5KyOHO^x|xN3_2gztZa*J${3dtH*i*mp5S2CcHKA%Lo5i&A8~J%Ccb1Hv z`gPowdrY>3_q%EHoveo1X+NKC)>)r+2#@5SjRyZJehb9Z*^I5-xI%l?ktw}~Bto^WtbvF;9m z2{w8<25v%bNB?_(DuKLvKKU+8`&TafD}McpWc`0vcAMN#mmQD90Z3?r9ogCEP4tH0 zSGACpqlI_t=E4DRd|QWft&nd84^0WP^Cs>!IxNDC=5HdP#nN(-aGaZn-UqIa$S35r z0UdRP{U(j#s6pd)$gX?hC+{RvrXJeP{aO_iaW;0npJ4}HzNY01jMqH(zu19}4=h!h z()m6E5d%KoWKlq?YIhY{!>rSx`JBA|x(?r}{We#-${cBdga+0tbVPC|U)F5HnrjYE zkT7ohjB?XogerhV-Pla9hdjb4Vh3f6|Ms?z0nzg5pUujT}u|-}@6tbOtN<8XW zTYxg32i!KDox;sASmT6GQQXVe^A-YTx=tt8bq%ao5oz7axA!bITg@6PCMoQ{qA^4& zthx^N_B3!C3JvNiTjc)Y-_x`ref->I|G8KM-o-)2#ZNkS;@m5|BPRLcQ(ew1p;V!_ zO!0mQyWXCb)jOmGTzsNAUY^$Wv0;ZPt(TW+hnTj*UzMP;SuavtNBjQN$Jg#~4bPV9 z@7zM(v6WDSlRicze|mD`qxT!XaG4JaRnM;pOHW4IlVS*+0dl_gfCIPGPx5O0yVJq2 zCyaf;>urL!M*Jco*#<_AA}~DrwE>gE5B9Qd>366vBVXUTH9jwIjVJZPOum1myC)k8 zRY=g24GC8X%D?E=e-1HvC=u^V_<*RFyAo|}Z3t%(nx4Q}?quMq@s&%9(3zo; z@@)8TZNqSL7o`^P?m+!hNbfyxY39;AAl~#b9IsYm4@{W)m?!b+ImYIh>zqP3T*F0T z-7nE-+_^^I-+zg`IMK02rg2F*WrnNaYWsXvaHYJfTw;kG3wiMxCivq>`O&%_R#H&V0N? zYW6jttcp~MjbNQ`i6rnh`90_dpe@{doyaQDoPr*jO42Qa5C9iRgoPW||BZ^usagcS zGU#Mf%?^2l_2uKxu2557{0%;cP6J(Jf3e|uC7wNY{zka%*4mI%0;0t$Gh@iwY`;l&ELs?UU*4! z#w!RksND6Mb|kzm4<3E{?P@11>6o^AjL670WA^?B-Pr6nET=U4H1t^t$8*o!$c>Tw zNm@&!ms#wwA{#?9&eEj&2a6I=iKm_8L)$Bl(-*9JS_Nrcyk;69r2A$hUmn!Y@RFAL zzAh&bZI)^eP{n(%_=cm@!HglZ4BLfs1Q}5SeT!O~iz#PHh8LIo#Vd_=Uaq|B@+IcL zIgdl!D(DFYBxybZ${?&?mYquu)sSnmkC}g=s2T&x12X!IM9MFN`ab{}*?+~c;DAIb z;1JH3Na*10MB5J1nB+?@q=5G@87mn5vCXkY142(n&ZX+82|{daH!UmHw=2u8$JAvg~2_Z?Y}ECs$k1(Hxks-%VLQiI@Bsq3CX@ zd$!@`T153i^0hsEP@As-+wS+%!4BG>a^r7KxKo|cywX$Ya!;G`Z_pSUDiI^I8j(>b zIQtYZmsrl?%XLKfhdG`|J4!K-H+0-)qD#c~H0G;oIgNYUQIJc~;$yXP4^F;AW~ zi!rAYVTSN9_l)y&=9?4tZ4*=v|I{-2V+FIUbG0aYrH^bZD_~nPU1bfhOdWI~%3aw# zAIY+UB0OAQ4LshpZnoJ4al=X6auJ8GOPRlBk`|EXCKPqih4i-kLGLjHkJllhEf?ao zUac`rXUs+>b-KCtpo1C@CK=@r;=z-k{A+xc5OK2=fu^yZDl^a|!}jHui!fBM7{p|x z2(`9ea9Vq5tPRI@hlJQ~xcyBc9K2Pqz-peAxKCsW|NWxgJTiElYNRS z8yU*cJ@F@<^gAtDdzmm>Dc^_z(*+>_M8#vj1Tspns_&VyxV)4Pk95?XwiC871B!gCWwNz z?pfWOz}UMK>BB47ja9+x7hE@^@@=_?UP#gu2FKpY!#i9)+_Pnp3e{k=V2i0&%?Fn#+4@0~dPsTQ_I%K&yVL?=J4e!fZEHTr z1!~69<0Zi(J>WJvVdnzMDd^*0|0BEVsXy|>aEVl{&9Fx-N2=8lS>vOQ_mINR$(*g( z`wYVQjT!zy;@Ks5@H0Ri`g`zTc*R5-0*1;Au*top^Yf}v{*2cv(c>Sb>=_NCYT!JX zqf#@qnqKssBSS=3=viqa{R-uUj#bvz{TkV^tYD~Bueeh0k0vGXttERX|2>UQ4eLQU zZgXGmdiD5pVn*y*&)mR!ZoMZ(QXRt{`aNsmPULd*w zSG9&vC4GpvRGWoywgBOZ>Z&G{7bS%fKEZh*@q8q?Fg0->0VgR;ECcCM0Hdeo{~ZXq zOGbI`Wog2lHcqNWUrh!Qt)E@PR`ovz`*;=$jE6s0a=c&JDp=LD7;C^XiwFk!oNR0W zpvwh#StEPT-zx$%G$Qz|;wPVv7M)DZ<_NDETopLI+RaG%w52I4i1RT)8|_P;{@NF| z{cNV?{9WRWsN6Hi>5CyFr;uvDpu~|pm;KdCGOfFh_=mK=tx3cMS&!z}-FC9I9yg6d zYnc|3wp?XE8)cdU_kR_!1f819>m_bvgVAz|(UMZ}hPT$p7S;UnrD}G1SIm+;%+_WpzXXPy>duanF;%JSd-_Ned1xfB z49S_A6W$({U5yQrM4&AYvNNwvnJQO(nm2=PFLQS9u}{cO%Yw!jj!5T3+46(J;6W#; zP2mow$Awo6cb&yCxKWS2B-@{LRm-H-rCH1^*8o z^nd$u099OmEFXXX!CfoZ6v4TFt}8?14+QSsrdW0vqJz#;$M$&|J%#^7cM=E~038W} z-{Mec^sny?$WHYZ*l9xoL3TLf8BP)#;pKF;edH-xKuqGZ|F%n;_nKP%czF2g#YU8< z22Y7~o^0M0W6gZ|uc7D{iXJkBX)LmD7K`9_oe6ArE;t}_rb@pP92;PKKOG#R84LGx zVksQ*{`7rnFVvufB3;2ouT_EX$1gr6($b)qtg~)GROOPtDsqH;rgQ9NCL7VAdvhy< zXkG=-f_+xW@YZ8LAE8hyT1AaA$9VlE%$;d*_^2#n>| zL+|i-oyd-M&H3CjK}jv}7@C__tQEHg3V^pNTjN4@u4nENe9_Ui!Lc6Ccx1znbf?k8 zR#y!OhD<%E^3k#>13n9GAHBvO(Xb&}D(g#FrBWU10YF3pA)=j-pw`pWa7_8`pTr{H z=-Y!wqGq0ExAt^5K6yrLex}z$w>mjyik~-Y8$}vg>H-NL{hn=K@>Ax8t_3uCObOgZ zM{)!#JyGaTRFB^>tA!r&<$e0V076R)x`x!KB`K$U6R-`=egwWNt?(-($pq5gdoOP` z%3Le1H+agBf^{+EXuAkBJR&$^CvG^KLGy4+jvOppuy%j3SzX8tD5CWl@;9K`(^<9DYjsLx~3nnnpNt6CtIZ zEuZ~E#FmS?fQ(<-j8 zzo`%kX!5}|u0oAl#Uqbyh_?4Regk2%z42)h(t|bMv03-) z$71zpB^x|a1S$&x&R|`ISqDLMh%E$2iRXbztje0*K{SV$@)&Ksxhs?ediqTbS@Yr z_04`f+)pG9|IDJ_WtW^nXt23K@JXposz!WZsMBMpZa)>>jF{1d;Redy@y=bE%Ys&t z41bP27nGq5vwc!Mz4dcaF2SYTPZ@8hy~=0NskCuJlqLH4GpVRF-_-z$WWV#QllR%c zKF{TFS099f{AP$urvzl1G_In6oX;upj_ck|kIkUWY2RzO5(#fY17<5CnY7U*h`~On z&%$^=y*4Z$sx5T)llFyslG|Yo(iQ!Y9(38#t`{vM3gOSjt>7Jto&n=wyMq@;*Bjne zCl}9P7?9a@k03Ty@&kU@Ul1}kyCKqS!L5kF{(q*2zdrtr)&J#LTt-E-Cjn17LvBB6 zZ-Y5wGB{5#DW*IUMT7uM5U>6J8ri-tD|tIG1gN-Vk-IlNM#M+T*cmcuRuT-1och#J9xzy8)Qv zD(L-nnH)k|&tAt*M?_t4IOJLXLux6JfBlyum3j>6N|;e#HYJ(#y7K77yGnB|`m=2f zu z625!SH*TCwS>}M3%5FqbjfjeWmrR{abtF^^FEdI;hT8XGwBqlvUiR3zZ$G6X?UOe9 z?%<%9hk1qd9py*NcC1~rHl6U8k+xpGloyBsCt`6w>tt~gH?bDZ33p@PO-G?o!4-95 zGl_#iuq9b0V2I>LgUs;Ex$1NR(fGn^k> zDa$FTzZ^bv&-vnY4*%?izxp(eYYnrfWHN+!2CgzqIz9#-PUI1NiYI>wG*|x<7g)B| zc4#VH3HrhYuPQAg_>eY!FDm25G9uJmL+G@>U53-ommOh}JX$oT?ELNc5sLxO(t)Ym z7%H$W2fnz-d~@JK;(E14P%ijjd1=jVGzUj7pRxQv}I>KfFR7Nu#Xp9A+W!>5*vLz5Oc(M*mj z=?}U}!tzQfgJg57%nib1KGO4lcvShei-L!HBWdAgHP7-=9dzjEmf8>bjjK|wSA9xV zmrF-MJkNeUB#wmi@piLug+08mX>5ow$5q++K=30KlRJ8nsV$3_qd)1b++3aVw>YLz zRO852|4D32*~0yfOo)qBgFaMfVx7oSF+4tVWFsxU@|}KXyb&mco3N@-D=ItNmQZKi zJd_1d&D0d%8eagiZvD=8Lkxpmxl1js!Ze9#EF^3P)o_pzdui!!Ep&S34Ld`i#B9{n=PMzg&3xyEVXZt2a&xmFOr# z_DB=m3vm7x3+y6R>OwSj!YvuTOqWKidfFn}^{v~$>Ne1irbN&|z~ePh9H!Q@DUT6e z&zUUm`$PL_(A`JTI9Kc2V1moWlSIxS5EWXL@*v8RO75cAVH()~9UESqdf^>dwHBFe zF!k%{)AK(-`Pfr|1$4?fr^k=*gLIRVe&^_~oZumYl2oLy?HE0oxo>SVaFGBjfYNeZPFG3CP9_xCAeVm7``4;3ywXEH_C*l%F$X4TJbvt?Zz zx+~n^vG0+SCYh;S(LISZ_0zJ}#zvJLaygQR&xzJL%ol;}JBFK%`Hc0}}j2 z1bZ)ax7OpGg16*ikw5NxK01h|0HR8KN&76CY3l*H_+!xpY2;Q*r)2k@_#PzDIOeI2 z!x{83%@Q z4)Lfp9Xt(@s=ZxAeGfC<{}YhFj5;t_lGFAY?~v$ar%j-L$mX(lvPzF$%en)RtD)DR zWSpx-iAXKfId^g7RB&87Ee_-Hp-kG-F{d@J~3%mU`r}3Y_08*&m{qAF$eiR+etf-siz1&v#(=7z&7yP$z zXWL@8y>jg-HL|NRm8(7c>LY1xXA-KPe8^i~(h_j)Q%GL1LdrM4(~WbJ1b66KWW+ed z;o`d3C4XpeXC-n1jr5b-KHw2Qqc)Lqb+)DMK4qWItph zPNKC|O4R(SI7aG&x8Zp?>G)*oy>ES`0$oZc%j)H)r=vf{>4EkeC=iM8<6p0&N7r&j zCo`7(_|cf50gNZSv20edDMV@kf{H0PJ(XMnoo#!~&s>?t2iCY9Gx(E{CAc)?8ngBK zRpO*EZ{X!gZuN46X7_=;qQ%EaNeU@b+4tn6w5=Ux&j4y(r=33TDDgM&B2RA@#2ccD z(PRM|xfDfJ(c3Ws!Q{b3!LN=cdcJiyY0swBI2f57w|I5<`6|=P6>^SbTRRYCj3x~F z;hvNg$OYWAX5B>~V24}(%8iK{;b{*RgE}t(Z`#EMIn2o#5i}FYwjsX#c;P<>2RZy5 zHWqWR^@hs7ebD}yCfM;%kn7CrConU!f>10}|3}4&wM^)N2iwuWeKfDZbv*g8rmQD$nMhIQ^bN9vsC_oW%|MVxAO!v(v9J*E!Wf zM{#E}p4~rt&^r0UCP4WyQFFV#I6VVPW9a&yN8JZgC3-^g@ct;?dLo-s=K@tKSsHv4 zSE7(|!r{`B0R81+eIk0AxOe%+-t$Z<4_v;!i>LUkW<@fTxmuJfg&mA+oOgD9;Z-6H zqVve@6I%B@k&PjI!_aAa8fSCUxCT>dYfpPtVAne3fVzgLLij$h?BTplb8cQ>#q%G*Qqmi;2<`4XS zNJ!;cLnI`lVs`hqBy)HE;^Ja&bH|gSU|)IbdCRopMaYmH;u^u^cJ&9u1pK?ADo~zR zvkP6(p4dpf9-+ejYhu9vx5c&LF7^OseoYF+ZKl^lhfxJ;fX>s8Qr_>lDn%X~^U=^H zGXs;>(_ujLBMiEjACmR6TbicAe3))(h|QORS1Wz%=vGdzHo2&D!;ot(UpuvA=|SBW zY&bGlQa~X;r!Mlqw{GqYkKp}|SFuF7vMi3y40_Ko zG_8ii*xaP#a)9LnZ!bxW>Ws;8Ynl^Hp4=_iZ>*GlYR}rMfYZ3VeO9@lc+KDaJiBb% z*azDML=GEs-@08^A6Qyaawd}eca7&~(@_TYO?k)M+lVaE zpm=)1aa+mMM@>Cl_G8R28K>elnr$>cF(_a1glN1oC>nVAW(Ek2_8JVTz@3<3JY7wX zZ)m6I3-CT`!+@myCtx)Yi1?`|R~dhfNJ%!$6+N5aNb?#XjML{#9_H3Skv`aMCnU{UZm`SNsrin@n)cU+rVOwS zn>-&6=~RpH33yI-OpE(^NK1gPxxG&%L%YNw&fEw*b{aO|7l(W?mLr%V>#c0yr+V3E zQO~m>|6pD5##;E2-gp6I->X_C&#;*Cs0S?OoT`4x-Bv5mO!@Mw07qCUrC^hJiQb1> zvW^i^@21+$HieqAG9J^gkt-nR`WS+aY7E%I5FIL>L*upNGoFY?x+mbzt?9|Dq5e8{ zUvt}eZq@_iyJTi9M{&{Fm@SCi?reJ`+Bn+@sCXn++X`r$DaQpEs3Ud(h`q&hPZyJ2 zjD8VX>Uy%R9@Ump=TYJ10}BGMqA_5*Q)Z-LkUDfGTpl^=)ifJ;DLL&^Rsnp9+M<&-h3!hp?`uRUH{5}O@Rz=^0W@)QQdzPsdKICPnf z8xwiBddF(s|M3)6(j;0VWMC{+QS=Y1mwKZ8O}LsAwUy%*JMv0%-phJ!rCS`~8~e~w8K2AddO$&kE-R){I1wIDXuoAaz*6K3I)+hCFC!-OJp zn-te4W*Z)uc1H-3sba*iNzClhDFCq$kQi`&0zO4Dt}W!XT>+MmC8vvSEjiO3^n-be z$0xXVaM^}HK#Ax$?AQ=JwN$r$|Jc3JE^9{Dpu!hGROEp=F`7LW+}tU7I!_?!ICpS# z1ZhV34?t%yW(9-Vq;Otk%*5xuKOsvch|noKJ6A(c>25)W7d?Qia%{<2VkrgmLx9DC zt8?lb04miR_-hC(HU5WNGM(v0Wu-m!1aZ&**E#MR=qUtWi0aqN|8&Q)E`BP4!Zyn` zYJ=S%P(U0jH|R~@H6y{!%!Kl9iPlG{AJra#$lHI%uz2>a4CRZ&4O3u{d~psJ#HU!_ zxBd#(QQY0z_wVu5)BG{-6wiAAghcgMbF*ffZ|zsVThbxZE+MI|WFYxKA6TrDo@mee z=|jU;iU_tegHA6BkP0F3#mbxfp~Q00x1W>9x;uh7esFeY82e;%gulgJ z-`i@okPy?>2@ZEV^A8w;=#$?`Bul<;M0s#aZuwb+iCtkedlO>AzFYoozt(UU>Mdwq zXTGUVWSo&t)e{@aP2o{eFvHP7q7V03K8bBUnzy8U@MP9_HLBwU6wc1c~Pt_yy%(N#EUENPWr&Kp9-ICuDV24jY z*5n(gE#+*gwesE9D0FG!nm5iQr)v+ZeJbUPFwjZ_E|`5o@6?wrjT9{6ShMZ!$A66j z)$jW-BPKCRdXUlO#X0FrNWsm-dm^jWHgd}Pwj68(JnDr2m5`HjJGIw|0p9veb>t$h zsF8|tr$v!zbZ}_jT0{S6GCWT*A!#H?oyxMZ!$4Wkn(c*;hKJ9|E-GSOQo}F$d9~AB zJp`r8)92Q>SD2cbQ@sy0oxaC+Vhln2h9%cd&$F+yd)t7`{Ns5EH9<$6bvgs`Y)mUUy(!2L<8*n(GHzPdW zkZ_DgXvPt-i)1-(ThZX)%4ZdtmdAU3(Ox{HOuPLYu`*CMKQtn4`*-nU%>4JuW6dX1 zye~ik4g|NORiAzW($z;+igtdt3N}tl#;eow?w=Sl+$k!Idh_MOtuXxD#AWZ4^@pUPX`(8tIk(hm zvdp%pBC)h-vRUM#4-UK-GIgugJBB3fXjj=fJ11B0Lz~PhUMA%S(aZB#;cJQ98sI=T zz>FO46YVr2mk8N>KVeQ{j=t((|Qo z64N$o=M9irD8jHHo*DW;qr1N5J?7oG(0W#eSd`q>Hul=VMpeMPL|iVL66PW*^Id!Q zv4Le^gd2P$2bxDQdzx3dUvm?2M@*v^nET%Yg>6O~60QXjvIvPwt8@*e)d!`D^a4<9 z=nU-1+4B~}c;D_hxgbRWQ@qnLn$(Zx@UGR+Oi~mbUcM%-Hn#^wf_VQrj`l-34u4A8 z`^1TezWEI;@1||dd{)h)DJ%nK_WYf>nOR=LIZbB8;ha1aRS#*Lmp_gqz5UaD^zR&$ zpg7*>wG(PXPP3Zgzhy*%e^B}(+RU#tX~ImEUPRlI4}$?b6>I%E5%@>$g?c#c8rCa^0PIlo}=nX}x2Ks|O{! z5zk`6TyE2;By(si`!-ZlvoWocPRyD!HkBC41;1{iORZs`(eS8J+juY4}$?$(hU- z>0|g2^$Jh%?VPg6ueG%o^Yq7H$KW9JEnwJ3ls7ddU+VQm7?xaZgTSeSuvwH_LDs!3Q&ca zl@yT=#fls*wbV;z%lh3HT-bkR&^sFfO`ZHAUxs1;&WzQM`h72GE)v^61qWpBGZ=rX zIno0SFjHGBBsSRhy&`t)`2~R1JI|4`^~{D~cptZhWT&gs7`SL3BQYbd7vm>=cN!Ap zjF)@|v__$rqJXdQZe7<>>Q?&EX*uJMhhk2xuxAeZy`)Y^$^hu z?a@`ZzMQ&GH^uNX0bBXV$7dZotK)Eg>`B=*P4+*_2$s3kEU=`f+;R$U{(FqkQk{25 z-^34SBHhHG$WgtPYf8?ZhF`Zr6ap^ve)m^wmmZWMU5|q-f61h^R6*UqgtAJk{S5xP zUx5B9VV8uCgzG7tjF+J>TEeeh8iLWCA4eZUVFp%1t}XIedW;k;Hu8$R?^my1ZIXxt z8|t>i85xsle{L(J?H3l6DsFF@|D!uUa$xvktSv0ue|K&BL=oAXK3*1M_$sW0>z2P5 z{;!YfmSAI^R1(_bIpgV3p=aO4D;*0nlOLvs%rD>4pVrGpwb5z4gU{;n7epL~`e};7+u@HYR#vv;Hk@(3%#yOvBZRPeSs8?}4Z=_ELdMRs=eyy%pyKt5?MP`1QzA31xP4IA@=*}2QLp=3vW z9E;eZMbx2nF1yWg(}!28EJ$vD|LjCPSiLaCq30;(3@*HoZs&XLU0hO%P8>ZEllPg{ z-TyHP6x-+B^{*CY_N{ctq&`4Q8L*YJ$nx!K24G~S55dQ7*T`AfoRmrSw--b9jvvG` zX2yHsbPFrja00f-n&TE2B1olYqQZ&nsMzgY6MC8;ib5Y9EcZRFJo&Q$%u5CMCT$uQ z2~U3xN4N?LT$ny#9uYG7%=jT=0y6K4PKxiBKCsr26TgfwWR=^Hsqkv+s2F9G9BQ2p zY-qX|5w81>xl=P<0aviXJtF_>zwgrC+{G@#P*;Zf-pZrNA4P(`;Gr}JWXrnjg`NzHzaV>3M;}9~X@K z2{*?TmLtw>)=Yr!P9>xE+m*PNkr@p@?lf<|%=%eF483oJkT4dOt)4yExR16sFREVB zv)%8RQSW_og>H6(=1e2$2Tc5ykjLIgaC(8Ye?=n7ye+j&uH$XC9$=OnzzaE+%*noD zUSr99O*M!(jY<%ym=Sz<<^NL;4uE5jnisrLegW|a*}HbA1=z|vpKY*a=MOvO=um-?KLyDO>6+Dr?^dlHYI_I$ zLerOzdEYZmLV3zl6am9G9nNroM1baJOUl%X{LQU=&(F1nXbKr+hHz< z=a)%)Nz6$iI)UYxW34F%y*Zm;tkD}F!!nWNmyRQ;GUex z7YYvWVi7rj<}1pMk7Q+(Cl(SFD!1QMdK}dqB@1m(2=IUcNy#5C z>WqO4>2nfAJ>Tc{&n@I}(pz&R;U8Kxr0OWyCKL0xrt7?sRPYQdefYwLQl~gt-9fBI z?^RuxWfFHDefOHAi0J`uokaswXAk@l-&*9sjN2#i&oQV^UMbjWbP@s>&8Ej$?N9X6BMuGQ?%@Rb<9)Yb!zmjEAi326C)7o%cn`s zq@s`0=hdnSz}z!)#A-4uGJ}ZVIfkFsxu#O(EPh8uGe+c!phe~>VYHs9b)&B<3xf5A zvK%>cG`(N`B{w#h|~ zasYVkTZ(WxdWxCvs5IqKYRll!MnMKpYD31NY`R{E;ATP_NZc%ABmeNJuUWDl*J$#e z_NDpC_?KDm{6o%RUm#!(`5r1sm>L-|(5^o_(g*wBYP|#q*Kk9qX}Ru|r70f>$gU zeg7Htg56xlk3PYq-00v}=sP>@etb_BkSxapHv3}2pVx(TYeQ5YNBQW=aJBJsO>3xR zptgpre#rPBDs}C*0q>zpNg~w)7gSSLz6k_rIf?&33jRK0)v%q#9N_|3)K2nf3Za_3MZrZ${RoUnbSW z1F=ui{IWGiU-#$~t@)#bz|}XJ!*DO)hLlzjdwjOmy5O5Ac)2f&0M*UfAzIpoJI-&G z4)vcEH)MAA1$cu!Dn>0>7hwIdqHlH)8-A!%Ph?kd^i}KmQ`?|XS(D#hx7@&V-(?fZ zU!OM36Mu4Rs|{xBu80P+WLtws@e*_^8C5a@e*{EGgJzfl_>sO{;?YkbEZ&Asq8L7u za*hq9-+vKkFyaQ~HFupE@(rwJ8cK6Y5O^7vx#8&1BT2&i-P+VEr{R6bonXJo)dPaz z%%wGaww!EPteY7CXB_f_JX!uP7B0M9GrIi{BqV6M^y0tHHXr;@UPiKAErh%d`C4=$ zkvl#(ET~6(MLHhxbz{{y;{5obt z=%$zb-HgTs6QH{XPDbna@W^7P8bP`$PvPCEv^Z(!->PweG&xRM@2(=StFJs-x{oQ} ziM`VuNVPV|*IpYK zNl~O=Tf{iHb811>#-Xk*5~h;oDG-oJ5k^}YzToP=&~#_s@dg4yc6w#7UHWbiYRmJhmHrMd%ObEI zutdMZH*V=pj6a5eP5N7i1{nP*VDrjK4Dd&PNLcER%)E;muId*Y$tn_5Znm{|{*+NcaEy>0sAcs|Rj6mAD(PNWk6uct?X* zyHojkX9Dsc&S>g;0a`A-AB##(cMMnJTL<3Ieu#cdQ;y;xfrLx{DI0$0n-tq=xpHn< zpWle}s`L4-1C0Lga1bUOr`s70A7Rd1u zFRf;6i_Va}qBj}7ru}mP@u<$Gwd|d(qJGw$7py6iqTan`(56u*UE%E1hH`Y`XAJvn zhVtMdnsIcX2481Sj`N56A}fxC>#L-QTzc(2$4p(pOg;1-s3ZN5 z<;K?2=|l6Z-X@tziA=B*5sLfV`>Eii~&+9#mC=m5LfL1k#333*=` zI1~cTyFViddCrES$TYlg1P-7E_y#vtMUNl;ICXs976<^ikV)Ifu6?w_J~f$i*m=Di zfZE)gmk%?$Dc*Ded!usSh2_XhqiBCngEHo~g?Hlo+g2Pvja{Kyt}T%lmw53yQcEkMuuK+@A1?;Bw2rOHJLuzhds$FFPPnj9(MZ! z2yJpS0Vo#QHG6nUD0*2?4%+*g2Hj)A6dhP(=lIQ2P#xperuRzg6lw>p1?<)WdiT7xMhC}hMl;c$ zd_K8ts3#6k9yI_Yncz(oBQ1aY6Kp#oZ$S1w8Tr~->VA;{|6E7!DmI&*#45|)hPxauP)FO_94`UET%8wzHP9>J z6HxHMn%|WQ0`yOk+dqI(gD4{%@t%9~-&#ojXNofO#NRqQ0x1spXDjWi(|EYuNH|vv zQYpddtB2+X_9uK*D+JfZGEig#JlyX{j9>*`$a=*9ogM5xx)a+|QG6?UPf0zab3e<< z^Y?|_EKM=UgF-c&DT?23rBM7rzMoH(Wat;(gQ`W3JP*>i6|uDf9(zFg zL*zMK-z@<apnLkv*q&-8C!R<}nTx6jXk3$Qs&=2-fCEIIbh)NvZ%5hWQ z`ej#u8tGHN-bC3;TL+=c2HV_VX`qE|s1GWkH3~j6nGR+d&~hrvnBN{>VZ=quWGt4f-3@0jw>kg-4UnYmx1&sOAx&2_zviDrt-^Pc%#(xoZ|r9AooS0 z-{5k1wEyYROUMznu^j6LGYdi75j?q32}kO@y7yMn&@ z!gh6FculCf;HFewc@WqJLjgaL^UD|B=@EQW3wX_aPD=n~)6oGYDeaSzbce)VIeqcNmmASR^wc(UPVan?sEUK z@HH&000+{dG^`Ga2@Uv#)ugXqQs`t$)WC)})7)1UEYS~8VHFzGP7>0W)6G~G-woo7 zD6kA37qz?0^Tqq`W)QzG|m;6c!Tyq50dO^3LC%@(Gno1{P=Q2lsHLg1&wzNCwGkB`!tNtcx zoouvxX?wAX@M`VjQji1uC@1xFTBOWqrbYza_SHuY0G@GTD{u3I*o>I9&DMaSNoaEP zCAQ6A=86mB?`tE=c-=Fe9`E}eD|D|;&-T%I^|b|gK0 zHoat;%W@xYH7-CV;;wRhXGFe9kP@oU zlOr~i_-sgWwF^npcBqJStI{uQxYbD#@B9+;Cl0sv;l}*^tbwa;+n^c@d33%AY{%R8 zH`&cv8oD&L_j7;`d^&uP%MLM z`kU>jqzi}gIqWu=gnQpNENooy0xY>|+s{R|toN1r>tMQNe*|aX1SOLMU-ZUQnYOL> zPa$!cLwO2g$DQ%&{Gfr}rVhQrv_5Ooe7=QmSV2-^ZQXmV%IP5xqG^je)(BIGZ_7k*ee-c102 z7ff^UsQ+KSu>U;QC`bGyAf_cG(&(l|aMVtg*w-7v9Zv@gp>0ki4f35iIqcD1j(vs(iIW_g zYV+sjRK{DEBC$ii=!+ap{@mlDFWH_CjjlGyDRZd`72%>bSq_={lAz?xw`Z4Zl_qBN zjb{*!bgkzvLRPUwtlwV%Qiebts!Mh0NNP7VztHgqFS(XiY`NDwLU*&;-Xo(YM9&GP z@7BN!M208!g(6Ep=#9Zy!$9gdW}Ch?^$DrzhGg#|Q;HS)7%GSVB8&6o5A2l{a%AQ>>7O88OT0GIdk4>WW}yvEDj z*B&*d#Xtc?yhU^Os$tH4fOlDY+D?;95#v|V+BfU>w9s0(Eod8f!Em?MrPnQ_ErfB+ z8#M*Of6zOJW{!}VmYaS~frjpDg{<$Jtt|sUaI_Lu3u&TRt_vYtxxn^ZoEx%Kl8g@aE9O*yQi7TQDdAdz3u`_ zq0I`km_63WSvDhPE#&F_xF+rjon)Msj?Pn_4RAiJZ`xVB;7bRVy3VVdbRT5dvGR+e zXe?7?pD8$Nd}P_{Qc$MgGK;Zt0V=`v>FEjIdslX;QVvF)enI7kC?Wqm{p=L<{Eahe zKFel7Pp{ni9nBV$|PPc(V*1vZKi* zS#^CqAN0k{bdq0ocNZ=9Ix8kVCVuaCwTMf4UL_S-h7p!j6MGwo_pl59y=^$v8LUY|TowE>}_H%$KK5=4f0w{!;#Zs2`FS%J*L4 zr;Dp$GiM3*Qb$fKrZms+iR2Hm#Y5y|#@xZZ33q66+Clds=>^Rk#uP0^e?{`>r$g-I*y{bGSjNr;5;!@#Ze zU}Xtebv@Pnw(O{FuD+@Et^?82oxR}iVm@Uav%AM!%oE(c7d*cJ#UhJ6l?fw?jz7*2 zq*vo_LCzl35~81n_7vX017RX`w~RoWCRJNU;M7P2(se2!AB6$1KmO*?MCfI?C_;32 zP4uVp(5kTAo{Mk1H|8gE-%IloW3)F^2(QF8x$p8{(?m+ux>LYh=}m9BItle1}_+FLl# zC!T^EI)43lsdrGp)WG>C=~$MRNiqxPO&i-+i+*s9a?7N(HE)upRst9#jo&iw68mCW zlW{;d&@t(hT8JQ?jC8zWu3@<@-VPFGl=I`uQ&5I!yQTQBA)(7Jr}s2BSiRiONY3vS<{8q zNy}%wnF;MC)OLWDS(c6aUsnG{gxPK!Z1y505}|$!>h30UIIP$Lp1FWI1>|Xy3yMIv71SCc`|X~2KWbkV*JvnO zOVvOR#>zW6K{gQuTB#A3WV=@TTxG!r;#pB%segq>%gsgK-338bt8hsEwoFPg1kqWe zmhOd^HOJwuS&6bv9Qljw!@SNTa~&cZWspDcL{C032JRCJ3D3HdK-AK=g9*nao$*(h zV<`B>(l3U#$`f3`$}Uv26?(Un*qtTxnFZy%Kvmy9?{p4c-N( zNfrF~M)Q71@9aaqy$m8DQ^A%D^?Hmg5q!Bs45BlQ0K`EaIK`)BiyjL-*LBY87n?D~0X zbD{3w*27C+$Zoyb`I0?~$srx0nkH6Hr+{BVamjMbkk!D!RYCu5f_f2_C>=V&^--F! z#T5NHg%(4Cra^N=rOZd6p6|;K{TmEbAsW}T43Me2rhOKPqEM)j_>Zg`<_+Z{g1hof zjE$&u#-!fTWY;2R+GpZ_o}{ifW8+4vbB%fK#Mum}>9F`t5?1|MD7_@9*1F`}={CSn zaEwP#GW;2M8R(|YER@9XL;e$$edzd4!Gtl=1^G8P+JmOwd1{2b+4>AU^YG6e=}+Yp zJA)qOj$lx9=IK!sFr0toU>HnStCgm@3y;QAoDc(n{M5Y53xdH4dtEFysu3exSuN3bOO83580~FacHY1;(D8@xmgEH zKBPB?CvaUL&@s52VY^g7@bvhcP1LPzH>j}D5n+&EWf$cQIq5k=1}1^!-6?}2|IX~z z`**t`gMbJNC>=T&h=?zCgqA*x?0OnV-yH59bUUpra2AK6j{>2_bGwz>^Dhj~^Trm_ z59VMya=b5c_j52ws7tD6*Rgmj;5JZCP2mLxeJr_TS?uIkLPGmCzJ!*={` zeDcFBdynkq^*8t5VZ)cv{z&5byTt8`mQHBcqxwT^>w6^XP6noi|4;tU$^-MVN`m)) zn@1(VymNl`Kw)RN?=*aJg}6Pt+*O-cWtbXoZOxrMc|8!o+!$eT%8^XFYMMHuRf+cg zk{g_rcfPyj3i4iJ2*c&xe$2UKtRkVVA;C`!MZ7~XjjnnmKe|hwI01OIF~#Nib*>eveM&JiHW-*T9xD7{gj z{jrb7xjE5NMOZ?54rT-Q3ZCAuMtdY-H~i)}|LQ=|PLyi>o>p;8p<-Z3fN<5p2j>xM zU+(urcuF79!P92gFMfLMQKnCf>X>$=Qrh(upVgzqhE~K&anZvG1f(3Ke;ub^Yz$po zEk&|yJ|n{37V$rumhg%Y{PH#pPDjQvFMlD3S{OX5^j zlNL{g+CQuuM5|;M<(dW-9R2>)mIYaL3P0I;`I&B3=UZ+>6%slcRJ zfA@pt0s!Ahk~a{!=%?X4?LwPQ6BH@%Q+vE~wPnZ4dyQf-DCTROe*MEfV&{{Ski{xf z+}x3Dv}>ZUg#YyJP}no8(uE%beZ{#`Gd+jncUZNIuHrsl4~3HG@`L7Ij|!((o}PE% z@J#8(X-Vo}6=vj+_DjFl zsqMi;dt?s4BvydeaMd^b_AOU|MG`b_qLgkqx+IMza80hx0N{;x$gM`q+7A?wXB*|eZ#M= z=KRcNR6wfQn~BYj-4Nd0&@|ho-{&$dD1EE;9+jz1v#TNhVUrAhpM`SNhrA!FOl#~X zPndjY_gimUe%?MqBUUxNad{B0J?X8gryX7Lsucdz@ege(eA<|4>491H^g+U1V*2SQ z)(kMVU$I1OYq-j8fAxWJnu6GVA^f^8PalM{%I;)j*d@XvY|y)y?tU9Z*+k`^gB~TJiMI zSJ>6-?W`Fob^tLoc%NcoEn4xcpWqny+e$X9KH%bVW7A0vJ!Ifx{ZT29LuVsnnSs0v zJ6-FFwqcY-oK@#ceoo8+tBrv`RKOZZvw-*F{h}7`_LjE^MtcbfwoAj*-p{RS)-_!V z2jw9O)ko>70&F4OOjQP>!evo|=UpqV{whT3 zsqL6l=WDo$)!tK|o1D)w%B{AjO>nSlpM)k0%|G09H&}Q^?bRkp#o~PUuEj;2W{l)S zJ{6ah4#3^|A|{%PljbOce?|Q! zFEye9rS%7#vJvk-#Z*6`z+!q$!;XSp0DaVTFhUKjJXV>WS$HB7Ut_8 zW8@{^zFu!XmDg=`;EBojOgd(;(+!pe2UJY8G zA%1As-K6V0e)(aNaX!{RP5KY~?hu8>&rzgv90RjL{eo7mvYK1mfbpUo9)a^+uE)(a zR^HtRTK|LQ7N?VLhHj^>;a50pCYv}XPrXtd-YC;TYPv+x(~zpRix7k1QvSjxCy6k2 zHxac`$1z#1u>clqdb`TC-@zOiaK%rtC;SlB+4AgtpwBa3Y4D2gd>bE`ecgabc+A@( z2FcK}V5b}=zjMHliA#43Vi!cn|4O7am}?5ab2LW-qWhZkVDYVmo^7&8?qa9sEXu>s z+3?VO$T&xEB&rO4HhFt>nmazTu?u@~^)I;B$_cYJjIMi1kG(KEvnNx@;Xlt4Cr>6 z!QT@ih-ptil08&P0Xl?mbo%#p>!Gj`tN^UH z2p3iMC|>X0s(e#lCw=(&%N%ci|1!XH2;r-8+-s ztGhRL1Or7bR+}8|#X^a@41SXIZY?YH8Y3T`F=Qf;d%HnrVbJ8BoG`P*6ZGshduQj>F=4)`}#{7h3bGr}R0NTlC zN8h;EX93kWv5wL)x3uZ^eE{y_1q(0G7sle-4Hs###0=E5OsLCivnpZba=w7)X=`b4 z&#>xEy%G3U0Tqz3J8v#HmGLp({%6I^N&)=J-n^De%k$cv-qYjXG)Z%iLeP!G$;$kv zYtC1_g=fodVh6eR&dp1=kOxiff1fE|@-GhO|Gu@G#P(t{+?>j>$zKzC)$?L|DPkdI zGSqsso@N#fe7ls4n@5pYh1$0-2Wwupl9|i0JE7Td=aM8{r`rqDx^AO1-MN6Sa=ojO zWe<3=XG$@^K8367WrR!|6#LMC+tF4gU5e|+@+cb<4f;7=igfk}f5xE}L9!?#Sk;{g zpN<)y=TR!i%&9HHw2#^+T3rmd&(okg7ZXQ{A>o7#vmF}-XMTR$;*jmlxF28rh5K>g zV8F)>pwR1eS{8?|3_9Q^6nT{Dqt)9+rq4c`jueZPt+emKleJhovORT_DXNCoV{C?d zFjLso4CNYD1#Q@9raWoMYivd8x=H1FCNeJpCo;7Q`1~hY2)Eq`ZUt|-?p5&%E3l-T#!-Sv-N(PC z!Iw40FpG$ZU)!>?otE-v!B<>?*m2KuS`NNZfNM&VGVN`dW93D*j%i%Pj`OXbf$)8q zDhUmB#DcMIGwCmRmg`R!l%r1ST3`}qG(RT3X7QiUpN$4kJV%U1#v+jKnu80~1x{*B zz`$nU#edV^alIG20|!(VMgMDr-3`~X%QKzOQC20ZcLKQd&>2~^fL1BV<0bf>e&3L-bdTMteH3NUm{8ZfZC@rj2NbL^3(XUR*BqP ztkql_%j*+Zz)FFVK;f1EOz9*&hR08Fq@BtssRvl2M~J|2oLlc|284diklvHK{Kne( zq8ULFE$DpS9$xRd_>IMalnQH`_Wu1k*>sGN^FuhA7#!JhI?AVIb+V(;@Qzo2dN%?FnzPviNT6@vyt9tm@x0@t&HmSZ}g=yv1Wx9nan z@5-`|hIrQ`F{vbUu|s2&Q(t)=1NS~zEJmK%-6V=>sF9L7-33&uTH9q(IvfQ2X;mZl ztnQgy70GRo@wm!#x#!(o{srwpl^t@u&K;LBN$gy&qvn@Kc!wzNQx3`p9vKtxX->8u zY_i(Ev0hp7fYyT+UXNT=o$veIz`KV(^pu9SCKm4^Yb*S_*J`a!3Q-_v){x|skF{XO zN&Rk{kLJMCY^~>HNDw`)NQeit>?|&)J4-?m(! zEGcCO%a1y>X2HF2JMV5~wOmUc3&EGze99Wfk<5LZ0%DRvbG9F#3@xt|haqCQM-VkW z6Qa~{1+2&IP>r;9tzYaAS_ zT0ao8;YDQVD!E${%4Z`^sXF;=R0( zd>+&|Qe2-FugE7K&d>)VkWDrJ!WwZUnOu@q{9~cO{dQ5%?cG#3loe=3PmgN^Mi9zq~i7j71|BXmg`=mEJErljeZ(Y#TJWrSgD!o2 zAg7C1bbG1wdMcrU-?MSifA-t4xz(*n9Np61tx&HHc&iKb$Hnq!BG-F+2ig3)W!8Ib zXUPH-((DR>0=-8$SDHlTQC;wMui6T+B(QJ27tr1lbo(lCY;hU>LF%Wl`%~+jC#IUA zGUOD6k3fueQGXdZuiVb9f)MS8l8uZ^k_E>Un}IyBE~ljW zj@c)yf}kaj*P(%Co-_7lpMupWOd(9?3wq^4<1N7z0RGF?kT%O9(qQaq|M+=;H0|oD ztJnx%tAqcH+3j{$q*?=qJgweKiXrTB2lweAXG&O>^obtgvp&E6V^t=O{JgHKh9{ni z^h4(==F%#)w<$txeZOyL3(8GZ@m3lt$t$pj=aJ9MV(TdfMwtOkM}_!P zdg!h>xl6JNMIWgw-9OxYlgD9Rq3vDf%PJ|fuc{oTOd_PRA9!AIxKk!vEkd-p?0ZzD zq91CM+DE8DYlb9x`$ppJBonmU#A{6ZT`YJo%$JeK7NhYeCgD>eJneZBe(|-`eX{+y zzSu|hbymia6t9lti&6$6x}0V+B^7}a4ew~HZKkb{3%w&s`lCl|bTGi*3iK%hXs)k+ z?@&)C;CMM8Syki5_{(3Y$rtsn-1exxy3YFkTIpigqnRk!#^!;?Pcp_WW|yBLNMuOQ zXX(T;?X}L09nnQ9`c zOV{4I(CV87uou@_(;3EdZ2-F6*XcxF=c1OJCBWVij@E9q-Ic|fV5sESg!zH51+pb^ zwqCME?ikf}QX9w#t=WECwhg|WZ>G$`5oU?`p1YZc+yW>g#a?$Zq?tyMPkp*4rHS#-{R!P9&q%sJ|x<`{1P z!e=5>(!?tpvj`Yg;q(?Q;Mu-vUZb5nL5wU#^CxLNdo?LDxs z^Ac2V3}d?bVzu0 zPLcwZJR95MKKW7+f5fF+S8M=}oEi`L?h7-j%1VU3*?3x=!eqS&GmaLo!&8p@+BkzG z*ZpqvlhS)~tVe2<+_le>(UO*Igt~Y4w<*Xts-grrW$09elu;ga{`ADQcFWNB*h*qs z#caysIMgWG+3ZynGyZ`Q)0>RF=}O%5*?CHOCMiGo%Xo)(Jik8C`ixPXz!a5J+~ zwHR5YB$)xK+aphOzMEA~_wfJ=%SpSjacH>X;E^z2rES1so_So!9ASKEPsh?e0%?2l z)-u_$3m<0JyMowRUOtT0I^FS>Mn(7Q14c0P0Zmta}lJbH0ce@`|?S|B5dahO33lN#ZW`d4Uv*@V|gEF^^?&J1`grhp%+EgFu-GI=h z;;za1$#(QU@~zvc@Q&Sn$g>Q+nw@dirdj~-*u|sgq-)y(CRA;0!ba07(v^ey&`lTXi zjn2X3^?hn5#`Z?y;6Ag*1}EKph)ZD~H;S=+!G0NN!}9SI`V%uOYjY`CQpc>TAGoO0u%0vzS!6!s*HCp-c9_1 zr`LwWrAT{tCtdwY`J=B=>5Ei%`pYR6@L2iVv#ct*NqbY!m2CXB(O() z2G{po*n5Zc|HOHkO2hh`lKUN_R5G0SrnnueT_KFbrTS%Ym(!;aBgt=c*J)!Ek|Bwv zG)`_PGk{y|$y;za=k8|Fl#(c=xE5gFJ^4&iG>p!=5~5o_(ny#K>*02ykyOl*vhbdK zu`_6iURJ&ZxbKn?qpFTMM4Ma)knqyws)H%ZdF?{aK>-=jG{Wt-Z?5DjnjwiUt5w2l z{;Q}CH|x!u0)V4c0;IX+pbd5mclJ(H9`{-6$%E!W|Fm|*Z&AVOe7h2BOiNs?k9}{( z<~4>__yApRWz&M`44cpOpY_G!H&;%;09J|U;IVcv$`Nx5qozmoa$D-@-mz*px2#dB z)6I|HX3<~N%{ydewiM9>L@V|n+0MymQcI^E`!ZN41TeyKw&+w?+z=doNo_SzXb#kd zBvsw|N4MT!h5wgS0n%VF^>(@Yv!4A&v+sW&ic<(#IC0o@F-TDe$-fc4tbY=jcMB`z zFt~npzkQGTyB4L|z(jTJ*K8J|wA0MFz^|1oiuiWFl0xqOPUN$+F~3=O0JE^Ir{ex4 z(bNcCE^6U3vF9qYN;6qdYE^ynQjI8yQ@L0K^CC3W+94o2#_cDjrm^pf^ok%GB3X$I z$2D=1rR2#3tv{pWWfe!{cgJu=ZVKSo|=!xxTo zbd!*jfW~?~ZwSPSZ7prNw!l=%-_kE7?!4I;OoJ4VP<`pv32;+P`&CxE(Q1K9t{Z5Y zLaAq(ueF->aIVB0{iAm>!l{oZc$3PAkMg6nDV1^Gq!hDut!avVvT0X7E7ZP5z_aS` z0|*irVe_4TH(KIzrD}qCEi857dzYI}vS8a$bkRi47m60Qp^p(a0>K}^q8k!dG)7hy zGe99yv;Fr^8m2vUa$0OpmUpbcpcQw1hp)pxKt26=!O9ob7SBXk@6oB77CFe=uCd?9 zOk-^g``W8TdlIo)C3+deV$+QG(Zo^5@wbKnFax-DK7%ZB_=j(2AyBFSjmo>OadK}c zE)W{qFGg7A;;~rHt(>&Y9?bbYkgG&jN}Z*T7ftUQJnf-Q!UYH2qE@hu+61&%l37lo zM<@{pJA}xC_N;NzDaj+>b7udUUp{&i1$$Zb)TB8Hf7QT>F*_m{SKH87(ylhM%Q?bt zUHK(BVZ#9-|D%Zac?@TC*W1Wr&V2$c>{-IHV8{V|+ITvCl_Qm}d<3 zpDMfcpwxZ)j|4frW?E+;_}9Puw`BikubB7NjHfelm!n~?zo4QqKTu(V-jE^yBziqA z*RMvW*FHhc?5DdK9!2NOw;s&yu#i?+D>x)Cc#Q`T&%@|*Xs#*#=r5+u@@7R19>B@% z2-Ox;xJY%Sx0M_tX1`sNlYCfzfP8ukmiT*hCP}q(@9Qxtd&P_{8wRJyG!%`Djp1=N zm`nbk@f{JxjC0GOc1@#=&;8E-8VUJ!yj`V6rZc-kg#qOwgR>@=zNO5HmbK^-^(v0* zefu;@kwC^{Ug`DkQY}Esu%47mD+P48RkW00+wFKU=mBILJyrf{Cf@1s{eQ6lT2$mJ zkD9vfh|?f*zB&;p-4y;C3bY&F^TMjDT)MWu=FWQ;L{`SnAP+On8tf8oyAT%U2{Gs? z#9Bw&aR)uM5r6U6S=W1}_zfvLsh4Pb@AMb5S{v9RC-U4q-XHLxb*HM~*^OGcQ8)D_^ zr@-EzfQco=ONua6!8HGM->%+0UCk?_Pn2%0w; zK&_o-9`fhai&Adu5CW?-_$TC40fpnZC3%3l<`(9> z^Mg$#=lv}~Q`*by{q`|06SLWPxPPf8GJR?2TQw7ky&I86kwo6uZBu08;~%*I@`__| z$zl#f=lja$RBucSF+RcH+Jm@8G?D7YROTp=oSh{DP`^S6;_YxqZpSL$C>Hx|RbxZI zP^K?1W9$1SZ@mc@>x734ShYxn;anrZ64gd({pVo`o{%7i8Pg}Pxn(sup19J&ShHTl z^R+}ACK-)=Dr$W0RCa6GCGb|2Ucam#qXj+SS)CbGm-=Nm0gTGmD5#x}#;l7^SP|)G^bjVBn)xtCNXShjb;FEw3 zkfTmvNGWo+=OCdB-CZU(FZE`Zt`+8fR2H%aa%<}9c0qQ}T_d}JVPH2GzTy@41f6m9 z=^XT-_rr`CXdf^OSuPZnDiEX>M7)K*hsr*w0miquB}BqQk$;YETZH9I(7MqOmBk9m zVVBy%%9~Aa`U#C4eAngaEx|ti$sfPQ?YLdD*ve)jDoR5n3-KVs) zX1}Bg4O6DwAXbN}NU6%sg+_w>?_6EU@b0&LzD)JBxtN$R%!92vmV~FI9bXR=VtL4} zK9_h@JoZWEW39l`jJX%))T%al*D!p4buTJ<$)3x>{F-UKe}q2)F> zUowD%E*W%fNJSyp3tK$S9fJ>c!%ow}$}q-t{*@(5jHg2& zN$cD?Qq_Mx{GTb3f1ZSO2v&F!BZyWT?e8FXYJGFRx)E@x@k?w~_-yII`OEh%&}!{k z%-X4-#4@pJWe=0eb<)IW>G~*G^@y^c@zddJD~Y|UIqYz(K9HSmwvU^7ivuYsaKqem z<5T?ve#7*YN)kOqe9lY`t*d^cEkxHg{OWQ4xE1^7xW!#&P@Q>vy1Ej>Ru*#~$tSff z7AFgxcX_!K?#NDwdxFYJd;odLZarjk9-{=gPXtT}q_|kWc?@V&qUEvZWc;O8rGLMv}ys@IAABle@$= z*mwS-m|ij?)F$u<$YVfSW7O} z3LOe+tuZZU)ibhcQe=z!GAT&>ItBBAdI@opEZFS2-1F`%17@NuW}b=onweNhe8rkH zhpw);lE1(zN}W8~WK*Kd-KFtkqSGUcE^Ds6DiCGk^9kP9T*xE@Z)fGSyb0DSKeZBX z`7mO8E?8$|+&PdLK;3#4;38S;nTeDED|NevAcreZO&;FIA%cGB$VUO8@Vx9EMeR$G(&uMhc&^J4X%3q_oLemlTX4e(xiHlDd~%tS;zaJ(^q{jDqH zbp5e){q{HozZD-n=xT(2&)+zAwroUN{z@uXyk}_ML)!4efA`y;VrcP(@kXeT{HIO& zA5ku+fhl`^ssaj;K3Ze+rs*HI3lyil7gB>jRJ}sm7OTU&r1Mudf^tQ?HmbzOId10f z8FYQNpXQu-q#E94r}FTRw6DsWizjQ|$G(8Gq{hInPCxlPs1_G0avm22Z8fq&n-!j* z=j~co^b~%U>ADz`m_^|?sr9Wwi-7JW9?sf`;a^q3$du_sx74#UL!voJAu-FTajz$% zTd(EDIBg8c37vPc*=wNd>?+ypN^$tQpAs{eJV@Jo$V;l-6-mfQBbqjeRCAeZ0$FQQ zG>?Oh=53`(?lqJ7tw(G$%%sv*zTQmZ-j8YFp0fQ?J0AnIc=kw|B;XSvgVyM2lpCll z=oH1^A%hmrL)ovkgM>fk&Lwm@km+If>(YI(y@CZ?>_;*@qXzCO)+3vX3Gq&BUOyYf zEpCYE)-q2kWDaGVT*{EPLD0~rHCP`E{iX`?5 z)>*ZnJwYWu+?T-2*z*=$d~F$E;%jwM578|d2=Xd>VhRc|fu6LBy460vsp|tC6u_zPMcf@% zTxdI5D--;%iL!Pt?PkIOD?!ysJza7k$6s)5QIb~ke+~fAL*KY(&bWj}c<1p%YZJtQ z=WgQxv^4id?n-+W&=%RO`im~C8J+R{GRRKmwbrR8--Iz*uk?1YHDvo%>{ch^o6CT& z1HRJsE#JBMy}GX1h2>(kSa3r%HH<2l??aLYv;t4GUR|Pg8(&lQG!4ADzqkvr^6G(R zy}HIWOqcM?X8@fA++M)T*0$^eKJ{L9=50jdX(TN^}=i z-Niqm{D_+3TLW#;c2576UEDjhoHOL(lqy%f{yt4$wt@2@^oE!;ZkeKgb2*+fu85w@F20wG z?v3au4SS>f#CL^6*jr{G?Ysy)SlN&B_gi>uB<1h8`;l}l$G3?;)mO#Tdgk8<$m)A)7mG0NcNDJH#Y&)usDy19_`&tCqMJ}%zwTlY#fXeQWHsUYXPI<`fNW`Mypg+ z)un4R`OLom%`=RBKVm@Et)8P2vli~6)^2i`{Y;h?!$W!OdRp|vnM_A`S2>$*Z2P?B zcdsxMuB;*G)~iu$&|ZVuDqhVgH`EuQdQ=xE9iowPbr(J80QCvV&q9M!GW_6Sc0I*h zD{tncySJ;tEnN?*VVN4WfMNExP42L1N$-&Pr8kR(QRx*5YBRFi${)bK+X!2iLgfAd z2zGdTJPQ!X%nX}#2iMjHxy^XC(tib>iJ!o^y-PNeA?1MNHa?+my2cMUZRi_b?StWi z(DAl;mqb7}WL0nhE^&r5F{niZ#rx&E;8=!SN?&RdDF5tgXL;A7@>8)onCQU#WAyrF zJ6p3wuPu1ptxqQvcxCYNQW(X z+Du)&*D4ND85y7?SSQFwgKSFlP*3)+(Egr~ntu^}tMOiO>Q%te$BQ8>aY+yi{OUviCZVKO}_5TD@ zNj5*8TGEYCe=hrU_QMp;_3NWn2C!s1-dic~%k1;gh2d1~f$S1oiI9@-l3Vz<%7?`AOe0ck;8m;jfa8ty)7FVk5F zyPZ1N#IHGeDLmpizq|ZJVPG!S|0Ob?ve5)KTadu!Ev0YRV~Bwf&O2%=3TX=qY+G~BJW<5!>I}S`^8no*`-gUl!k_0X5KKiQIu!mpq;BkSPyHH$w%gr z;jWZ+%{2PG-_&q)wf7(q6euC-B8N9~$>k<~;o%?~Z7mH|^yFJFfAM=L>i9WphRhRq z8*!8l23mDzc)}*+P$vfVp>;8JI*9}d=5o}PZH{9(ln-U9H|M{k*&-F5ICa$^hd2NXToG7{j?5GuB1!pTiu7$@)-&u#+w(t zo&>%aDFZD~m0(U)Fw`AnbMe78rh)T$H=99Mj2B2=T#Oek-GTxIiDnYqX7QIX{PUXo z47ZFix>t7fYe^#82)6Iz<^yY=&9@w6UZ&3Fr zF^m>=$92-<2W|1H*<}$O!H@j&aw|T2{E+6Ki%SZd8LA_Ljppf;ZBp?Cj6-DP=s)eR&Mt-a(@xsUM1n7 z4_qX^x(=QV&fGeUGAfZ@ z$f!57EJmqW>iDE*t>wRMA6Uy^Sl?w$n=qI#|9>M*|6_G=Q$h0S=A0qT^fn5QVvQr^ z3hkgVU?35k|A3TAtG>nMiA&OIWaqqoDlskgk>!n1_UwLvQl*UAtWSwKaK7-zC1v|_ z$uhEK^1p0S?JtsL{7NcZV46b|BZ4s^?d!_)W_)7Y($w&C8&WZ|B&> z|K)nXOF7a*UfAmr2OAyS)fq|o*PgacT1BE?-UV>CF+Cz};UG_RrSM-!_U)5csOY-h z$o%s$PKpe#dvs&=;3+mW`E61 z0YoaQs!oPMjvXdFxn5SaRA6cH`3vao``j9j6A?LsgVy-arCQ1)mj&MyO6)l0!)V^y z_l<5Y#VBeDn3t;@a!B!(f4FhQs*v!AGb~|u9I{+blMCZcWO-8{|Jtm%imm32tN!sp|ksV6u;{Tyk_}5(wQ^%CO zTu0p@?*eYJm@1{2Y_ABXzd1U0Sper4QcrIlF@xzOW^O3%lGdW2XPk;&xh91v79scs zTM|^$Qkjgr)yE^TvME?PPa|(xM)_IoH8T4qsJO=LqDq+gWOJ(KR93S*TD)mi#Br@a4aWqm) z#@xJ1=1e7sUP){J^rc~Y#JgCdC@63zCZM|LySPh!l$`2IoTMnq(X7dNTCq4mNk zx)*C+`2;6{fEuiuwo+bzPo;lSV8&Srt*+WdcI1IK)A$5}PUkE=VYM-fR@h^BI^lU% zK?gg6p%n90Rs#`mcu6eFU=Z&ZgSM{{u5*HWwwKwx8Q)tAfu@YnXTe_#!>*Wcb?&}Ge~dD)2@R{% zpJP~Y&stm0y#tUe-X%>2p+G(8$&-7;GsVz<2~a@N@Wn!RYe&j6x__j`zoq*>QW}>| zD(j)3dv209tC1>a*)R4f1dLY8^hY5?-~sDiuA4-7!kpPXi#TUt8g{poLMEr=$kmN5 z)hIuSEPd)?H;X(q-}o2i$3|CA3ntptEtA)EevqZ^={U8_D~JB@*SQ|Ku9Kq`gA&eS zsoG^_McBvNtlTn#R3}pS72QSAFZ={K%{M;DT&(lwh`(>AVr#1dNY9O-*`YDf0u zk4T+``|q_nGX#0|^9T^n_tI8`-&nstSVpua|NF~?*EWhgQ=N!6%tYg)4w^;&3G(S;!*A&P92-MDYBO{P`L(v8d9Z!le5 zU|=V?{km(lFfm*T*VA!X>N_1uS0Tn$$bSv}VMcomzv@@~7oOtEI96)=B{z?=BZC<- zE3EL5%Rgd#;4v%LS#KLxmQ^p&TMNi3s;qd|t*a@P4TU*<`=wbp+`F z@B1D2qGhw}b3^19c!4W=eIG`eJWgv{G4dr}9uQ>R2>?$fzty?#=T*}Zf1Ys%?ZW)6 zR_T#tkU;g5*PEfOP~O#G_-HDgz*<}JTT<6<)a#9ntKfiEm@30R)fwFUkiNt0S)$yO z`QPrKpF*N~(F}L1YivKRT5VjVIoP@z;cA7^wy(W}Cq4mE8`hYwuAY+?$#K~3N543t zxt1Z?QzaF%{f2>g^IF&jtFX9@k%J3VJ?EgoKbI^|nTi)8-8U`JN!@@qh@ILbMq;7a zHSDyP;2K(L#1(!1_?QeoI?p||*|D#;TEg6Hr)$x*HGaxPmA?%3*qE>d_w_lh*HCIb z>4cwurwlzYN7HPLj8tyix?~v(ZnvuF%+iAJDSFuzj?`bA z(g%B6;Z2rkt^`bfTk1&%_<^Z}BF!iqxvv?wPk?P3B_!VcrYpB$G<*@n{x`RJ^?LOY z%9T_rUOqMaRbfp&sq!;O( z(2?FtAPG{W2-2%G5v4=u9U@XfkATzwp@$x7LI~xu@4e@I`;KwO9sBI_YmKZml0TX6 z^FC|N=b3Zn8&6~1ZbMNlFM)-o znbuDFqxl+ox(4W4_UCqyV&#k9z`I7(MHilEs7<*x?fY!X2eaA>24WQPft7WxI0;gl zB|dMrkub)@1JH)I=*Y^MUZ=e!;-SOvBpBJQM{Z?G?{emv9d9je)jM2zVdb45CZ&Yj z3E*#99#?(7B2JTPbq3l>AtObJrP6yENp;VvH5}fiU0x15dK%y2ExVV#Q-_{L7P_Fu z85_;*TC9iH+N8*4U1{^;vE$%*B-dqgwrxUX@p#7{CMb&J878w+V;Jln4F3&Tci0k+ z^)~m-*&ZD96m=~bhwRiNy(%1_W-E&w*RHYL?EznB$x1Hdr3q{;$ecc9*Fth*bLcDH z?2ugxD`^l4?%e}6wgO#1dLN9X0uPPuFa1NXA)~a74MGv>C+^AuuY_fqgR78km+;-; zAwf$Md;iQp`X+ zhvVjV0Vp7M1P<+F#K$VE->nkc>kI9$i;n>u_NYX4I2hq|pJzxS#=L^1a@?7N6K;<@ z(YIu-e)p+|c9P}eYg;azfkgGdq_HV~g+ZPPVKz z7w6XU$?pl&ToJ1~YZ15A-$^J?^f{@>VTm7kSEp~}RcwS#C()m>`qGF_Jb3Zu7IEbL zyM0`|&2Op6od(7|BG;S@v2`LU8czl`^bGoPz*&$&ZPGn0q;=2{PUx(l)ifNb2 zX+sHa(t(rL>R(aJ!n1EbQCY-TI_)j67dLJD~$`_^z@IBzL{SU_Y7EN1TQQYP%9m5q!R8w(V6ScndYzsU~19&64G>HbRXXb-v2-MH5K-3we z@xp3NKX?i~c9F)=0cM1^P6RY9K+Lz%{jYFODd7zkS@FhY%=qt?WgS^Xj>9^JMAfH0 zt;mkeQx4bGg*lzKF4e6wslylZ7x;DiL4Ut1TCH(Jal=X2@cL3PZm+QeC#uT)Pwo5- zBYqaMoTfI%ul$>-cfDm8QK7WglcDhs zjmJd`ySn&+As+g6qda(o{>fTh_>SD0C#&(1T?h7!GjsCi&+R{1r)rq%t8z!82A(JJ za<`tpej-MEM%O14r5`G?n*K1=yw5UH?gPE!+&-~wy~n2Eleh`9>Bumzi1#%izPiL1 ziuY-Hj)@OLM_NEHW`4Z8abTz^OJ$OXNm4T|*{z4N|4?p*Y0qm? zQO>z`t#6nodsqXum1n1}$N$hE2a5U+z3I*xjF&pu0}1nP{w2yG`OaS8w*spZM_b|N zK#GoSg8|wL=I1HNn~_F~W}n3dNMoNAt|`_0eC)s3xUlA;54KBi2GywsOUV1KAzwpj zCNg=?R_;y29*3eV09DtxG5Jp9BA02HndOA zkQFE;OI_Y=+&X`U(J))vHaH=83wR_h9H)|R!%gVzZV^FOAZ8#fn2hX322?oPaQo7uReIl-fq+Q@!U(o&4B0dyLR0nasQcz=BL8 z$uLi{h{3@6w-IuDvF~a+y=PqfvUt0AU6nWsk3dAZ68JQ-wyqAM=@m-*;tPckw!EaODSf8TeI|>~bm{ z2X1c`trkKmQ8n{C$YC)_dqOE|7_^~?AF3Q>ZuSQ&Uy*IYl`YPc6xixu8>WQiv(9}sHMRbt(}3ikPL=-x}j-g@kM?x z-++xfSwl^ZN8W_CsTp&$Q`S+(?y*U~m0EBd(gL18C)Rojd4x;bbRa!(su*uD`{OQ( z1H$1KR@%)AQ)vXO2)v=;N3OHQ*0gA6uUM+H6ky(e0kQ*c9spe&d&>aa=@^ud?+S4y z3qTa$p#-MZ_uRFp9Sbyfmz?*Pa#agzh_`j_RcT8Igk;uUJ%-KuiXAJCDoB+ z6K(OVEAf_*gx=7sx(uVviCwjEh5H_6k6JI#_%(#(UV%wAlPu(T@6>=7P;QvTw0}YP zJnSWI@rk1UpA;GTEQ=`Mzb~H)W+6ZAC&-@ivlM~4>KrJ(FlAPO#28@mehsOj3#{UV z-0#IfNrS(HHrRf*sOr;)G^!MI?wVi{oVM>4ud-^%t$M>Dk82o8y(p1kMJ zs0Nq8%;UBK{@|S2o6D<}|Kvjk)?|1bvhr3Yj_Re?PDwpDt6R@@glE%(bA$Fem{fDc z6|=jV4GGTlCXFlgm{)rb#t+PY)}vZmLiU8$zqSRPie<+~Qeh%m`7PSof{!;wmz@DY zWgW$O(Iy=r;B}jEk8Ntgs8=nXccFgmvgyh@?|iunwv{>dcPb7_8geGP2go4=kly@n z;_!d`$oV+SeplXk^3{!g(Wc^^n-A>TFT?LYk7t1h@Yf8e^)ZuKI=x;IYrnCyMyoO_ z8Su1qH}HZsJdU?zRb77!%%LPNAkJLN#dibvhaHd=N}5H)O0+q~`A$pakM3x?YBA@t zz}mM6UY#;K=C6u1?Y9By1EH)oc28m}UIcsfdQjz9zL%4onP&4XbETA(>|LKwK|W9s z_i`zYdq=!XLo0vUx#P(kO387hh#ep%sNB%cEQ`%^~M> zg%w#%MWsY~X#*a8xh`R))br(q==*}HdNQkzD^*|Qsx=dDtEQ#D{J?*F{5jcaDm>W7 z>1}J=Zq{P~Tb|8ayBMV2$Jm+!yTEHAe;ryLHJ~*6 zLx-k4@QYo8R<%()FiU10?=Vecm7F;CX8h#{O?UhYE_P7v&SDbM?z~xWvw#Rk_d`6*t${%{Eif2LA zJIn%o7iwH#xRZGeS?N52=PxK=p)+80FXgo8O}ucnzGDZu)A03oh%D!VZc4?(^c_tY6-BY?l3bY8^K z)iSwmyijv*vElKVFfRV9*2Co$jk172IM)$GOmuJp^A54iv^&{)wU*-!xsr8)ZT_8e zyDo(U9|9<0XGY6{|4A+WtLxnVhcxg1xOct*zdB_~$i3i<9?VPPNK=E&i2eW>ofyV!y&6 z?A)t>tU??;m-}%*DA$sGH|DKASqQnU9B^jW_E=LBO60Sr!2!*RfmKV~3XIz=YG7x9 z-IMHV_&nx`nkE`uuxAZwT=1@BvN=jI&=&Uq1h$`~WhIKirtpe52WN8}J??TYP2_Vk&ZNQDJP)a}AkAOhSb}NMz&A|D%pYy}?uz>bR~p_q z^=wDZ#XD~W4kP#3mK3tim{tzdx3ai}Pe6oRv)u8+3O9ncJEI{KanZcqM$9#UYYs$A z(+8bcKu_ivqxny>2%f=w+ZIS?*}c{U2QPCeq~;(F1j(E5-OB|hFU}T%k-l>W&b9C@ zZ0-dJ0mN{juNCg1yR~{W`#ISp=_f4+kFpUECjD;uxn^BECu|zEu%P+0Eg!uisu?-% zZ#bAtH1to(2_A*TnBWTV$B=H{e_O#hNPc?3bh&cHkOuGE_K||byEr^(m`k2Mr`;T1 zeGQdiBfdzx+*HmN0iGSdGyR@|5iL6rKI&8nXdsDsNxDhrOv5#8r4Bi{f9HX-t!|c@ zt8eC-Q?1{lPjZR2y)7fm-Q}WLCyMs5-m{8N!#uV97uClaCjcJG52>cvx?_o>x$aq2 z*$!_S+*Q5VX~KJ(f>a^3%j)CEmzW=_j7qnc)%2SLXH4mGdjr(3eht2~4|(frG)A_K z)gQhsZu1+SP!@imZPi;f!{f{I^KsO%2l)OgFzZT9+1n&>dNtKh^_+_H&)q%`m!|$I zG4tklCLo$S$Vj}M-b{SJS=H+9EmNyosqbK3t{T;`iYyGAeTT5;&-@C)NGfdC!uC=ZN>KEDQU0K_-@Xv~nnr=UAV^{h~ z)ny1rwG2q`wQ5FoqX~K;<0GW-lgvm`v zr6|h3-Nt*)VdE9rwadcgtQoW5>$}_fIS35q%nm}dP#a$VO_S{l?s?TA?TAzf4%u!s zi}pB&X$C|;J=<h4%$uzwbW5 zq{j?DO8}fg7Or}({jEzdbL>B>COGb$8ovG8Ir87t9oH)?Mlzj`psvm7jgQ5Fk>P8R zRiZMSC-}7k(0Kz9&3%)EBha$!g!{MC%?S1l=?iR|S=U#~)Ib*aI6h5>!**HCFXUK` z@0IMIzdR(kf077Ai%;w%Fn&%0E>-C)4pn{S0wsNR%{_6_5UDS;t5ohwP-Y!{J9$H8 zN6$I`QKQP#be$hj`^-AKV=J)k=>r&rdejXl=g)Ddp}x}XTFx%{dRi*2sGmmolCXi_ zi6lDp56!yB;RR(RUaM7Wc+WDMl+3=xbuOc_1K@UDf~1=0 zu)Ed@W8(cA-NuKn-=!L-^ioq3Jplb42vynm0na7+>wKy{KXhoR%3;^UQQ|sgfiL?nbB9UD@|Z3Hnq^9h zq9P?JTr0b8+tp1$=C-F}VJof-1R33xGSGHYqwK-J177O1!Co_i%!Y?k6}__)5e|;( z15tD@r~Xv?4%tYp*LP~cS!M=A;_FXNMg^avY}Rs+CJh9WRk&Cj9Gs&ebpeUSIMugr zkZVUb?oHUk0J&Yxi}vm$U_S)91Fml<33iS7I2}qMP(!PCgJ3UZO77V!86>NcZU_-D$~1u^h0HS(`bIJ ztsGO;aA0IY-jM<0&r@Ct1httckr1Or&X2#_f5E(nII<$SeN`@#{0@_Pkd$Srv*N-=2~PvfS#IKO77taq8ozxY5_l*_zE%Tbao6 zrmeIswj>%uZo{3=q!TT}*>IP5Zd!uHKs}UIEJFvP)4cYS_gfU`aZ)+U*y3MP z39sJ)yyqw%Q?fCvQr~*F8t|Q2lcU`*R21?lM)x~S)$bZlszEvM)O%iskM|6DR6yZ| z#LK2ef@#S)u5AKS1lD-aO;*l1_%7shW2qq#O!hwDJxeyIxk%7ddoMK z+w3@{ROjl&Gr!tk7Qk?e`E}>nnp7AUkzF&iUgY?WA476lr2-`aZj3j)>H_5CJnOR{ z%od+|a^QPBhl{4q%sT7`Nb|B=moxULWaxBJ$&5H-sjv=q-}=i8vO{@zB?v0%wnN`o z!QVa9GA!+SoGbIEek{6OCyd@p8aWYfq z-TCaGFV)fvIgwt*Oq(iR&7WC_88+f(5mJ-AU_SqPVV$?89lOsZ&tpdE2PfMWob3pQ zB))vPS7$d_DjA5i!3#1|We0WcIM)HuNwAl%8PCj&_xR=fQELUPW*o!fA9UysF4^yF zebLz4i(O-E>pcGG%^m1^;_L4p417}lGVG;U*94^-hDl&0|0eW+Jf-^l?;0~Kkf;|x z4hw`G(EMkO@9)!pDWFevh#}pRUuQKh-XRgK+)FTur*a#OlM!K1wlPhvK#do`@2vem~Sw~^mjc-5D zF=B7ZHkgjE3)N?ReYducpe84lDxV-u8NW)+QCxnLSW$eZ->BrJ8+w|Xkem3`New@n zRr;Icg9ay)2s=yhU8)Z>rO$sY2Z$MU*a4@R_BHgMej!R3>5^4nO~0@L;O%poD(M51 zTMp=nOqINh>AJmL>BIHqQ{n{gad&Gs&FF^QusZl2ct4dlL;hw{^ImYqRwC&UzD}Olal)KS61Msc)EwD{2{21H`i=IS>uBRoQr_vQh8sj5 zjD}U}R6j?Z&Az&`XSpfvkD-|I;ky``k`O`IdqbfX&ZZp9y#wk2^&K519YJfK=bOOm zE&i|rgI*wDb{v*>({*-M3DZ1@em6NOLl-X-wB3UF61eLO8EMe%J#}n1BsBB;&RvT* zCk1`fx9a8sE@1(c*JeVTvs;%Z%Hf(S#U%QfS_F^mnmb=r0{2Eu`xO~-HF7z~4;*b` zr4%|FrbK1|$olkf@A3gT!))%VORGHh?~zhAMFSck#7E;;Y}!q0P|fqdY6A|I-#WiA zgJUZo(U);#2cpcHkfXh|nBW!GlsErLhxd7uEYCll^7zyJyBJIqA{+NJ9&#vo#mn{E zT}~C~u0eWy;C2N~TCEE{-N%Y9g7gZ7h;d4%+&K1^7^_d9n{QShWe%)zC4UFWyn!St z)l4TGxEMcHEiNA_=Dae=ReYqbSo#V$V_?JrMDDmW3;MFadSRLK)8P_7xdVNTxx%II z-42kC41dpa$4J}0vxeKZ-zi@ZK!>HU8pMH^$_=B0|%j*>kwH(d%Mi0!iK+Sx52-aGX z<>?*YMqK4xTbQP6zr+!r&YpO=yqK44D4E?JnE3))To919ewmB^V&l6*$lk&Mue;M; zGy@)O4i?Fo@)|Z2fpO*BHdJ2%12?#w!W|LowLT2|6?<}0BChqaYblM*C_@bOq$;^Q zT)W!CbZVo;9_i-(6CIyS-92O=aU~k$e#m2*_5)}fgmG?*9${k>;^EO%$xkYMt$fZF zds>u`knw@$_dWfG5#Rdc^4R=sULqCkl4}#pADUy+i)qB-D|7t2oQ2Dw*tG*;yqfwhiP9d1OHf7@qtG%&p}dl%N5giaBgI_S{?aoK=6 z&FA|93e8$Bfx)7Yx3{O?Jv-ah22TxCU^Zr-d`od?wsEo7-C zhq}ADyNduWwhWmib4AAS=dQ;K>#cZ^?0zzt%SH5+iuh){;@3y-iJpdBF9BZH7FXc_ zuc_Ut|JeG!C;z>L=cX)v26yEg@KlITCM5Z0_1fW+nITS=tC=!GXr_gQ_WDxt#onnu zJ3mUE6qt3q!AlgSl>ol+VC}YT65-*58a8q_TJL>7Ql+WeKP9@tErDDv&nTPN?sH&sjwC##rV}Z zJZq@Yc$0PXQe5-^`G6IHw+d#a_XzzX6!ntLJ=^(x;S(E?^~I1~)j)orud*C&%$>cq zY`cR^@P)op(~X%m6%+qI2vBasrR6jIZ{l(S^3MO!3&4pA#ugqnCTj}(zMmaEH71kH zhUH9JIZ*n@karu$*BZw;eBGziAGMz0Mjx%jFts@#fq>j2-?Eq0e`5yEtDd&@cja&4 zs5(=Y8a+^M?^BbB$~he{FiOf7%B&gI+Yd@A#oPFX4v$hkEtWZ*Ims zJdyTx6&^W_-VMwVA2$%tn`}N^n<*($k9xA_%!U@%<%38ya>Sit&7^(jVqyyj+l6j> z1#QkNHE*Z9Tm2Yp*e+9jQH2k)S`IMASm8?7ZGNF#XHhR&8-y^rvj+&;o&vh4ZM=ho zi&U8)wITBq^2aO4ATr4=KkMTuo|;^w)B`N-JPyc1DS7Qb53F=^DXZ-$*gg$>aYc^% zU@>}kX5;-7AaJUWKrn(8i6Y!Yib}`oXXfVT^Q5luh~VmG7z_nEU4k=t;_?pRz3n-u z5r6dfW*+65rA2DdLw%0esbQ$BCI7F?Vh-Q?i#m{ zxn8@1Oi=`GU%!8BZKuhzx)>Zc{Mj{OU9k15ohoGTefhP3Og?_~R*}}s^DVluef|WQ z++;7ed6L64G6RYbUHP6{hh5eE_b9*Ldw$Q-i1fkb98%|pooM0)2Gr1^#xW%C$Tg;- zA3UvDR1(F1_Twxj)v^!Tpj{Q#+)z~ot>_EccB0~lkKj1GzIXcKj(3?ygHb^-)H(BjcoNSmrWS6P2I zHBu(NtRO3N_StS!v=^J9$9 zod+8ljB$4EuEIxsB?axiQX{ShOn~}kP%#tTtX^-ujQpfATylNja{#+=Pkdm*G%YFy z4kEQv#LYRRYfE-^0t1%?L{_~1C@qu97)#F9XZgQ?%*9`xS@gP&!!A?sZG~|Ux2d`U zcP~SGew4OeyV@oE+5@@N@Cu|tFX zL4K{`jB&DTj(A$*{>IeNp}Tc4boW>i>^P%@sEsMvpJSK@_7O)Izl^Z(Z*w}1Z$s~? zOdofR3fm9)-(AzNYkYbC#;$s)eJ+v944a3y{axkjf{c%h>!=qy)9j(ZcaJPR&9PUk zZ%In-^P85o{}!K0yRpk&l@Y6P@ale|x5hm* zYbP_5XLh>rCW$E5FE!;?xvJAF_7dUrQT){0V=3b6VwTd%0=?zD%Sq4KL*M~1`??m3 z_jik%zVY&myq_WRP5$jV@<4pOn@hbOo#}7Bb6aco4-m`3L&Idq$;jRQ+RM|&Tw)+y z`{9_}gjw zS{r^GWQVN-lHQ^9@ROo1cqYZ1Fx=5$trXnLbd{K%y_{LGAntMS;y}Db^SsJ8;pjFK z!e+7JvRHsM`h%+44<@G3cz3(*(SW)<8t?c%{da?rUR!T_UDqh`ux3!iHe=sG1xL#P z-|E~bcLsBqU8I=seKvsK8UiC`z3 zk`!(-()Df2(#C;UByj}>?~z=kDSb5)H;M`#~Kbhi~B6sR?ha;UR1YQ zY}-~;ST0F<(zUv}UXzh!CI(mgN|1}KkNf;Iw8nUpmgWh!OjXx5p>_NKYkzc&$?g`g zjxc)fD;~%=AiHopIJ^)x1D(VMF5G*0^Yd%A-`9&)^R<8>;G>2dy5qFH7bKJ zFqTAo)+!qeJjWC~1naxf>hG7)LT{qj90_ZUu#1Xe7``)J^Se8Wo{RuU$_9o0wsIuC z{+r>`?lSe0cf2W%hR+)g2O>&eJf^UsrnpVJuBVz%Etg8IQ^wGbOHZ!}+p#*7vL)@K zq)*Nn4%X)`NUG#Wm~J41i6gRj^-aJW_o?nSv!z3EVYIM~KL_a}-aUZa%%SL~6e zMgRFq%bkyuyR%8}_vocdqZbisWU$+Ddw-VNmZQx+>Ne#|EmyJ!y3R~S?s6pS{oZET(|{(-QCvA; z#I;M7Dn6kE7w+H|!<&1rWheD?0`t2y$-)c?zfkl|5vl{N&e_czwWiU`$hUG60w57B z$&$)q??!MxuUuwTLCukb0U$QQe6kbQlwENC;p5rX_X^(%lNJ1u$I<>=K=2?Wcjc$+ zw3>R{Y%J;%lVm*jttKk^P39aMdRwwN9TP=J^fW`3+-90RdVbK|aVV~q{p)M?{FAI) zo-AS9u5tSQ;nIQ$0!^(mwlBIIpIyM)z=#?aJ|K6utNW3|rk_T?8@ZOvWZ$Fa#$bb` zpVy*4FqheG+*|T5)O+l2JubW2IqcIiSMB^8ak_Z-$m4u98Bw*(^a|7{J!1js{o-Up z&{`B0@^k(_Z22GH8%%wV$MVtCtJ_%dlh>-f z}m_n%Ot7+1VF83ehy$nFIq}&lmkmGzyRfZRxuVrE=MOxT5jxu+{YZ z-R5q(=odySw}6vu_EYR(Q<pCK12y||N8o7n)8a3ji8j5Rs$Lc*wf3q+)z9be3V%0xba`AG zr+w~CW>;}fSKo^LQTesR4~QR-WU@^PQ97PSfa_8NR1u$la44n^v>F&(_#o!gbTuV& zc`zA4wz)@-5CJmIk_I|&9=3Zno)|Arrtw1N0RCX4*E2+l21zbQWqs~ZHU|Vg?V0;Y zV1NtOuwns%7oigm=7NxLe<#bQIde+*lc18cAQ9+ZP32gb{mKt8VXlzM|TGTr{h=>6ksVZXU_I|@7fH>Ur6{11fx7d{35ya&1> z1mg{ngLq_$H2%o(%N5CTZ(dT7deXshe4kOr#ZE0^Pv&f6CBDS!psdU7>VY!1I};X; zQ?ZNathtJ9N4=8{4X$7+B+eY7z9;z85leVPe3-dbq0HC(&i?kVXtll9Kxs=$Ozb@qmO{r-;8>e%)tHciUlf#gg1= ze}h(pqv&;@r*S@C&B!j1Qg2h_xuFBexX(W0^?R~F95w5mTlwjGisuOZ;U}z9aIMlP zjWQORA?5(hAn%Ew*|gbsLaM^)Z$6Tyop(A(zCU6qT5WH$xySDNwxceddV~2?9)_NxV}eCQ@W4PbKEIr^7<+9#A5R@kb4D zD=G9V&4Jk(^ju>oCM6!<=;FjL7@ZE)UcQC|pyI5G?gC%t>!c|5PjhdX4ycyHW$Nl7 zBYQubd5l`9r7xPXb7&VjNIJSd5FtL`o8~~CX}OpIba|d!%D%UWO3R>^*3w<*aGQP1 zb5WVqAQi)lFoU|53t~YP-Y0C$m^+fQxYn=3f`*D&qesoR0f=R!cIHX$suxTuc_V-^a|37(nmtM%0{|e;nD)?_U{~3r9XkIOSTJ%hmps7z z`NhW>N*TX4d1(3Tg7~biM5e>x^%XK%PR5l5sYu=<9>}*iP)SGiygP;h5dU{j?$=#M zaH>=V0z3Jy1?%5m{io>uM+9RC!M?zQ^EKHl%_G2HU3+wZt6AA+ba*ED%2Ue21lh0` zhx8Qi$jFOJYFozSUVzF3tR(hw7#~r}F3Qe}QufGK`H-0SW?SW2 zfRtQLdG6U!$5_`R5&bjkljQzj{8d{H-?P4_dM=5gLII323_>Tyrs9Qi9}23)G35wfuB zy-P#=<}<%gQI@Q3&tr~tqaM52MEK|H+(jWlI@NK1jkf|D3T;V^rKX&(IMmVCJMLoC zcP}I`DBM2gd1UcogEIqDAy>d~vOys+=MZ_H8SAO|r!QZ(o7HvsJ9f^$vR&J_ciibC zCA#;Wr(U^~=$=mhu30m8V$%q{)-uLBXd_ps zaZ%!H+%NKe`6PrDAK$vOT;=o0=)JRs0L^NYl^R{=pV;7o@<&;Y*CT1$LwC=Hn#1oX zwF&|G>6PdTWLBPu{L#oh(+&owH5Y)VVGC?Hvq6r9X^}f8$1BFRZ0K3wh>)T?Z&A4) zrIPf@TL8X~dj*tG6>*U*+mHu#ot;9TjYncxsq+TB!ZkZ$R&sn1>tMLKk}nM!drGTw zYmh~B{j8BEeZ3h2BgC8TaQ1c=ne9=~;$jEW+WUUS&8pWlo{xTWd?wiKKAGF#{ zkVWs{=__8xVSjSHg^wQ&=#p|!tj&1QRc^!RyE-}j$4a4B;*vZ51WK-X9o2UWou?lZ zyqb47IuahcMc+BR9F52h$^M=`zf@JkdjFfPoZ8KqBj}}?gdoe)k*=s7pDpkYwQZ$% z{r=m?H1AH@w9J*hSa0t1udx@O>|*YHf0BHW%0KeZfAz67ab$vPQL zqTCs0K3+~#D^8!>CR_SF*63JrZ!M~QjE7@#s{;3N`m~_n;?4c0B<4R19d|n|qdRva z)5PSRo&)!NQKY*iJ5S?zyKf;rEqCP#DqIQ;pQeD=6W@0ijkw*V9>7u7$nO_VJJ=c! zwh0?h>wo~oWdVW1asi6Tk1yF6so-p8b1PJp!n??cwL4TX4P#zz+1sjIHo@cypb~5w zo5}vg{!l61Std&!FDgEAx<@`}x$39U-XBvGe^a?liKNp91HlzG!BRT9OeN94J+%7F zbbruN=5Wv|L&D2MbQ_9EG>Ow??=9zPcf;ivMY1#EWDSTFwg3ZAgpKEH_@2>9*ejV4 z*Z7Oc^%_~*b4VVyA7XjDxn(6Y7N;od+J(WTIDFUI7Qe*A7`(JU`SR=$`;wE9B?bQJIfL*wOz9sQ8!8fh0>6M*SkXkdJp4$*?s@VDb=AY@ za@(=5{Q4_)cU8iVom4o#&&Y>c$&Hc>>8T!R9>(B@k6uLI?j5u&sCpK3BkjXg>W#i9 z29DZ|v2RqltW^_r>-eh+&NKLS&8`P&=u$@9i!O4>4iW1+N z`O&3U^kwK?>gin3m)I9XIOSi8VE+)RTKIFPC#`~9bB|EM3bZQHa}SBw&z1XF?6Q>y z*c>QyK2Y7D=&NU;Rs3~#Z?jwW`dXE1OH)@y#S@XKdi+Vn>OB4Pk5KhTCVjNWja;q9PRB+*P`W^;ELsm}>um~Ui_OZB=>2BYoP(1bz#hhhhWl=VP%3sZNZLruPQ zZuHvmyfEmP8%M>$gi77l$``c@a5f!M5-2i-4XWFayNp!sOEnu-S~@0s-z(;v+tXW> zP=Ymy!q}4zbRwCt0_|GCN^vZB=x%RUnjqnYk}tJQ6{EKK*dVXq^X-{~08oG;i*r+6 zni2gxtRs&tqVMB}B*E&qBT*DT&I>cg=6^gSHVK(y+m9r)-oJCO@*Y%=!G!x;BAzv; zL3<88+tH_Sc=Iv`e$BeM`Q`)K^q_{*VMw2#ymuer@_CWE$uT3;wL zK+VyU9ld-MD*LIG;J*OeE|%Uq;Hk5yzJj0k4y2qvgf#;`zB%o>+L68Dzl=ZKs{Z@B znS1BP)yJ@ZqUry{RQ^l)@?UI%^6!Drtyc(s*|YruOyYu6;yKxk_46c!WwJgIh$ibH zypX+%2p_IRLr#6kayg!tFb@j-e%*N{ed@cI{Q5MaP$D_O#ad`)NS`I#$6ke4a&lzI zC++La#Be#TbbL*g;w}|WQwB~Q>Be#^gHj`{nf4FoN3C%AnzrkE-Onf0;z&juf%Y&= zhNU1oCiWZC2uH5+Uo`A#Z%9I1yd?q^$3#9pSeNhNMBYZ(HQbXrB;m^{;ckpA=C6s^+l+fTd zmNP8bL)PnbJJ-y|apZC8UkTF0++O~3`wN_3o&DZd^T{%DRzvzoZ^n34(%O-yLVTb$ zc)2T8Be9%vmumJ_sPe5*6(3q~8|OU64_+!@Z4I(L$(t@N*7XWEM{fnl_(Wc9h##79 zivxlLN>xX|jz-wSm7prtC*64>c8bJY6Q4jSzw~NUwgB)=Gr4GN&NI%7KQ?c0mOZT8 zzfNVQf{Nd+vq){c1z6}LnhLB0RSL2h$CC0rfgI&MvqKO+ytgHpFO5l?2G6{% z#HOI57_iI_RNgfW;r59Zp-VapDgqt1WE@PF%#g&r991BN1zblkcnlwG;`Yy0pobBf=3(pn@V0h5d zt^>C1alq*(gO9=tdxq&mPBJCMWu#i|0C}^bjZo;uhw6XN1d!Na&rNj}G8@ko!qV zM++;_;7=7eAjGF5>rAT3M7*;R!1C5>dp8!AkJrFJ^_{1jZ zME0vhKaR`su;Y4US2Q5m(kHsvY(%V z_S3oCLQ@c@sv4x^QROFn!?}+$zx)y{hV47q9^P&um0z+6;DCi(VQmW=Q} z`o!f»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 h@at@orZM9v$B=I(jQD(U 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⁢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

F|?erB43E_G|fd`lH(V=j$IIw^5yAONG#c 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