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 diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 7da9628a..3913159e 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -29,10 +29,21 @@ body: - type: checkboxes attributes: label: Participation + description: (Features without participation go to the backlog.) options: - - label: I am willing to submit a pull request for this issue. + - 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 - type: textarea 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 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/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..94744d0c --- /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: 'has sponsor,needs sponsor,help wanted,backlog,security issue' + exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue' 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/Localizable.xcstrings b/Localizable.xcstrings index 01c0c90f..ca7d895e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -5,43 +5,84 @@ }, "\t%@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "\t%@" + } + } + } }, " %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + } }, " %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + } }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Да ли желите да користите режим INPUT_PULLUP за GPIO пин. Применљиво само ако плоча користи pull-up отпорнике на пиновима" + } + } + } }, ": %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + } + } }, ": %d" : { - - }, - ".dot" : { - - }, - ".gauge" : { - - }, - ".gradient" : { - - }, - ".pill" : { - - }, - ".text" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + } + } }, "(Re)define PIN_GPS_EN for your board." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "(Поново)дефинишите PIN_GPS_EN за своју плочу." + } + } + } }, "%@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + } }, "%@ - %@" : { "localizations" : { @@ -50,6 +91,12 @@ "state" : "new", "value" : "%1$@ - %2$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@" + } } } }, @@ -60,6 +107,82 @@ "state" : "new", "value" : "%1$@ - %2$@ - %3$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ - %3$@" + } + } + } + }, + "%@ - %@ Towards %@ Back" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$@ Towards %3$@ Back" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ Одлазних скокова %3$@ Долазних скокова" + } + } + } + }, + "%@ - 1 Hop" : { + "extractionState" : "stale", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - 1 Скок" + } + } + } + }, + "%@ - Direct" : { + "extractionState" : "stale", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Директно" + } + } + } + }, + "%@ - No Response" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Keine Antwort" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Нема одговора" + } + } + } + }, + "%@ - Not Sent" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Nicht gesendet" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - Није послато" + } } } }, @@ -70,6 +193,12 @@ "state" : "new", "value" : "%1$@ (%2$@)" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ (%2$@)" + } } } }, @@ -80,6 +209,12 @@ "state" : "new", "value" : "%1$@ %2$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$@" + } } } }, @@ -90,11 +225,30 @@ "state" : "new", "value" : "%1$@ %2$lld" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$lld" + } } } }, "%@ away" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ entfernt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ удаљено" + } + } + } }, "%@ can be up to %@ bytes long." : { "localizations" : { @@ -103,20 +257,54 @@ "state" : "new", "value" : "%1$@ can be up to %2$@ bytes long." } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ може имати до %2$@ бајтова." + } } } }, "%@ Channels?" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Канали?" + } + } + } }, - "%@ 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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ конфигурациони подаци су затражени преко административног канала, али никакав одговор није враћен са удаљеног чвора." + } + } + } }, "%@ dB" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dB" + } + } + } }, "%@ hPa" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ hPa" + } + } + } }, "%@, %@" : { "localizations" : { @@ -125,6 +313,12 @@ "state" : "new", "value" : "%1$@, %2$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@" + } } } }, @@ -135,80 +329,340 @@ "state" : "new", "value" : "%1$@: %2$lld / %3$lld" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@: %2$lld / %3$lld" + } } } }, "%@%%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@%%" + } + } + } }, "%@°F" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@°F" + } + } + } }, "%d" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d" + } + } + } + }, + "%d Hops" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Hop" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "%d Hops" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "Direct" + } + } + } + } + }, + "sr" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d скокова" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d скок" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d скокова" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "Директно" + } + } + } + } + } + } }, "%d%%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d%%" + } + } + } }, "%lf" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lf" + } + } + } }, "%lld" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + } + } }, "%lld or less hops away" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld oder weniger Hops entfernt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld или мање скокова" + } + } + } }, "%lld Readings Total" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укупно %lld читања" + } + } + } }, "%lld Total Detection Events" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укупно %lld догађаја детекције" + } + } + } }, "%lld%%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" + } + } + } }, "%llddb Transmit Power" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb Übertragungsleistung" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb снага преноса" + } + } + } }, "%llddBm Transmit Power" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddBm Übertragungsleistung" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddBm снага преноса" + } + } + } }, "< 1%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "< 1%" + } + } + } }, "🦕 End of life Version 🦖 ☄️" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "🦕 Верзија за крај живота 🦖 ☄" + } + } + } }, "1 byte" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 byte" + } + } + } }, "1 hop away" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 hop away" + } + } + } }, "7" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "7" + } + } + } }, "8" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "8" + } + } + } }, "25" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "25" + } + } + } }, "50" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "50" + } + } + } }, "75" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "75" + } + } + } }, "100" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" + } + } + } }, "128 bit" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "128 bit" + } + } + } }, "256 bit" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "256 bit" + } + } + } + }, + "A Trace Route was sent, no response has been received." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Трејсрут је послат, али одговор није примљен." + } + } + } }, "about" : { "localizations" : { @@ -254,6 +708,12 @@ "value" : "Om" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "О" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -312,6 +772,12 @@ "value" : "Om Meshtastic" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "О Мештастику" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -327,32 +793,89 @@ } }, "Accuracy %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genauigkeit %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прецизност %@" + } + } + } }, "Ack SNR: %@ dB" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ack SNR: %@ dB" + } + } + } }, "Ack Time: %@" : { - - }, - "Acknowledged" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ack време: %@" + } + } + } }, "Acknowledged by another node" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потврђен од стране другог чвора" + } + } + } }, "Actions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktionen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Акције" + } + } + } }, "Active" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiv" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активан" + } + } + } }, "activity" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Activity" + "value" : "Aktivität" } }, "en" : { @@ -391,6 +914,12 @@ "value" : "Activity" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активност" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -406,22 +935,76 @@ } }, "Activity" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivität" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активност" + } + } + } }, "Add Channel" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додај канал" + } + } + } }, "Add Channels" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додај канале" + } + } + } }, "Add to favorites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu Favoriten hinzufügen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додај у омиљене" + } + } + } }, "Additional help" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додатна помоћ" + } + } + } }, "Address" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Адреса" + } + } + } }, "admin" : { "extractionState" : "migrated", @@ -468,6 +1051,12 @@ "value" : "Administratör" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Админ" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -482,7 +1071,18 @@ } } }, + "Admin & Direct Message Keys" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Админ и кључеви директних порука" + } + } + } + }, "admin.log" : { + "comment" : "On Serbian language Admin and Administrator are the same as in English, but in sentences like this we use the longer version always.", "extractionState" : "manual", "localizations" : { "de" : { @@ -527,6 +1127,12 @@ "value" : "Administratörsmeddelandelogg" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дневник администраторских порука" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -542,21 +1148,57 @@ } }, "Administration" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Администрација" + } + } + } }, "Advanced" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Напредно" + } + } + } }, "Advanced Device GPS" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Напредне поставке GPS уређаја" + } + } + } }, "Advanced GPIO Options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Напредне GPIO опције" + } + } + } }, "Advanced Position Flags" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Напредне поставке позиционих заставица" + } + } + } }, "ago" : { + "comment" : "Three hours ago = Три сата пре", "extractionState" : "manual", "localizations" : { "de" : { @@ -601,6 +1243,12 @@ "value" : "sedan" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "пре" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -659,6 +1307,12 @@ "value" : "Sändningstid" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време емитовања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -674,61 +1328,218 @@ } }, "Airtime" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време емитовања" + } + } + } }, "Airtime %@%%" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време емитовања %@%%" + } + } + } }, "Alert" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Узбуна" + } + } + } }, "Alert GPIO buzzer when receiving a bell" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Упозорите GPIO зујалицу када примите звоно" + } + } + } }, "Alert GPIO buzzer when receiving a message" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Упозорите GPIO зујалицу када примите поруку" + } + } + } }, "Alert GPIO vibra motor when receiving a bell" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Упозорите GPIO вибра мотор када примите звоно" + } + } + } }, "Alert GPIO vibra motor when receiving a message" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Упозорите GPIO вибра мотор када примите поруку" + } + } + } }, "Alert when receiving a bell" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Упозори када примиш звоно" + } + } + } }, "Alert when receiving a message" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Упозори када примиш поруку" + } + } + } }, "All" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сви" + } + } + } }, - "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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сви подаци о уређају и апликацији ће бити избрисани." + } + } + } + }, + "Allow incoming device control over the insecure legacy admin channel." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дозволите контролу долазног уређаја над небезбедним старим администраторским каналом." + } + } + } }, "Allow Position Requests" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дозволи захтеве позиција" + } + } + } }, "Alt" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Висина" + } + } + } }, "Altitude" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Höhe" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Висина" + } + } + } }, "Altitude %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Höhe %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Висина %@" + } + } + } }, "Altitude Geoidal Separation" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Висинска геоидна сепарација" + } + } + } }, "Altitude is Mean Sea Level" : { - - }, - "Altitude: %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надморска висина је средњи ниво мора" + } + } + } }, "Always point north" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immer nach Norden zeigen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увек усмеравајте на север" + } + } + } }, "always.on" : { "extractionState" : "migrated", @@ -775,6 +1586,12 @@ "value" : "Alltid på" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увек укључен" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -833,6 +1650,12 @@ "value" : "Omgivningsbelysning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Амбијентално осветљење" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -891,6 +1714,12 @@ "value" : "Konfiguration av omgivningsbelysning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања амбијенталног осветљења" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -906,25 +1735,86 @@ } }, "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." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отворена, off-grid, децентрализована, меш мрежа која ради на приступачним радио уређајима мале снаге." + } + } + } }, "Any missed messages will be delivered again." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Све пропуштене поруке ће бити поново испоручене." + } + } + } }, "App Data" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подаци апликације" + } + } + } }, "App Files" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фајлови апликације" + } + } + } }, "App Settings" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања апликације" + } + } + } }, "Apple Apps" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Епл апликације" + } + } + } }, "Approximate Location" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ungefährer Standort" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приближна локација" + } + } + } }, "appsettings" : { "localizations" : { @@ -970,6 +1860,12 @@ "value" : "Appinställningar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања апликације" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -990,7 +1886,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "New Node Notifications" + "value" : "Mitteilungen über neue Knoten" } }, "en" : { @@ -1029,6 +1925,12 @@ "value" : "New Node Notifications" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обавештења о новим чворовима" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1088,6 +1990,12 @@ "value" : "Dela plats" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подели информације о локацији" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1147,6 +2055,12 @@ "value" : "Smart position" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Паметно позиционирање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1162,7 +2076,30 @@ } }, "Are you sure you want to delete this message?" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Да ли си сигуран да желиш да обришеш ову поруку?" + } + } + } + }, + "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?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Да ли си стигуран да желиш да вратиш овај чвор на фабричка подешавања?" + } + } + } }, "are.you.sure" : { "localizations" : { @@ -1208,6 +2145,12 @@ "value" : "Är du säker?" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Да ли си сигуран?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1228,7 +2171,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "ASCII fähig" + "value" : "ASCII-fähig" } }, "en" : { @@ -1267,6 +2210,12 @@ "value" : "ASCII-kompatibel" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ASCII способан" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1280,9 +2229,6 @@ } } } - }, - "Attribution:" : { - }, "automatic.detection" : { "extractionState" : "migrated", @@ -1329,6 +2275,12 @@ "value" : "Automatisk upptäckt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Аутоматска детекција" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1344,10 +2296,24 @@ } }, "Automatically toggles to the next page on the screen like a carousel, based the specified interval." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Аутоматски се пребацује на следећу страницу на екрану као карусел, на основу наведеног интервала." + } + } + } }, "Available modem presets, default is Long Fast." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Доступна унапред подешена подешавања модема, подразумевана је Long Fast." + } + } + } }, "available.radios" : { "localizations" : { @@ -1393,6 +2359,12 @@ "value" : "Tillgängliga radioapparater" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Доступни радио уређаји" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1408,25 +2380,80 @@ } }, "Backup Database" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Резервна база података" + } + } + } }, "Bad" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лош" + } + } + } }, "Bandwidth" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bandbreite" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проток" + } + } + } }, "Bar" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bar" + } + } + } }, "Bar Series" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bar серија" + } + } + } }, "Barometric Pressure" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Барометарски притисак" + } + } + } }, "Battery Level %" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ниво батерије у %" + } + } + } }, "battery.level" : { "localizations" : { @@ -1472,6 +2499,12 @@ "value" : "Batterinivå" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ниво батерије" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1487,13 +2520,34 @@ } }, "Baud" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baud" + } + } + } }, "BLE RSSI: %lld" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE RSSI: %lld" + } + } + } }, "BLE: %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE: %@" + } + } + } }, "ble.connection.timeout %d %@" : { "extractionState" : "migrated", @@ -1540,16 +2594,39 @@ "value" : "Anslutningen misslyckades efter %d försök att ansluta till %@. Du kan behöva glömma din enhet under Inställningar > Bluetooth." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Веза није успела након %d покушаја да се повеже са %@. Можда ћете морати да заборавите уређај у Подешавања > Блутут." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。" + "value" : "尝试连接%d失败,你可能需要在系统设置的蓝牙选项中忽略该电台。" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。" + "value" : "嘗試連接%d失敗,你可能需要在系统設定的藍芽選項中忽略該電台。" + } + } + } + }, + "ble.errorcode.6" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The connection has timed out unexpectedly." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Веза је неочекивано истекла." } } } @@ -1560,7 +2637,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" : { @@ -1599,6 +2676,12 @@ "value" : "%@ Appen kommer automatiskt att återansluta till den föredragna radion om den kommer inom räckhåll igen." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Апликација ће се аутоматски поново повезати са жељеним радиом ако се врати у домет." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1613,13 +2696,30 @@ } } }, + "ble.errorcode.14" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peer removed pairing information." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Радио уређај је уклонио информације о упаривању." + } + } + } + }, "ble.errorcode.14 %@" : { "extractionState" : "migrated", "localizations" : { "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" : { @@ -1658,6 +2758,12 @@ "value" : "%@ Detta fel kan vanligtvis inte åtgärdas utan att glömma enheten under Inställningar > Bluetooth och återansluta till radion." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Ова грешка обично не може да се поправи без заборављања уређаја испод подешавања > Блутут и поново повезивање са радиом." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1678,7 +2784,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" : { @@ -1717,6 +2823,12 @@ "value" : "%@ Försök att ansluta igen och kontrollera PIN-koden noggrant." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Покушајте поново да се повежете и пажљиво проверите ПИН." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1775,6 +2887,12 @@ "value" : "BLE-namn" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE назив" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1833,6 +2951,12 @@ "value" : "Bluetooth" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1891,6 +3015,12 @@ "value" : "Bluetooth-konfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут подешавања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1949,6 +3079,12 @@ "value" : "Fast PIN" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фиксни ПИН" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2008,6 +3144,12 @@ "value" : "Ingen PIN (Bara fungerar)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема ПИН-а (само ради)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2067,6 +3209,12 @@ "value" : "Slumpmässig PIN" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Насумичан ПИН" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2125,6 +3273,12 @@ "value" : "Bluetooth är avstängt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут је искључен" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2183,6 +3337,12 @@ "value" : "Parläge" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мод упаривања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2241,6 +3401,12 @@ "value" : "BLE-PIN måste vara 6 siffror lång." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE пин мора имати 6 цифара." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2256,16 +3422,44 @@ } }, "Broadcast Interval" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал емитовања" + } + } + } }, "Button GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дугме GPIO" + } + } + } }, "Buy Complete Radios" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Купите готове радио уређаје" + } + } + } }, "Buzzer GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Звучни сигнал GPIO" + } + } + } }, "bytes" : { "extractionState" : "migrated", @@ -2312,6 +3506,12 @@ "value" : "Bytes" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бајтова" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2327,10 +3527,24 @@ } }, "Call Sign" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позивни знак" + } + } + } }, "Call Sign must not be empty" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позивни знак не може бити празан" + } + } + } }, "cancel" : { "localizations" : { @@ -2376,6 +3590,12 @@ "value" : "Avbryt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откажи" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2391,7 +3611,20 @@ } }, "Cancel" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abbrechen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откажи" + } + } + } }, "canned.messages" : { "localizations" : { @@ -2437,6 +3670,12 @@ "value" : "Fördefinierade meddelanden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Унапред припремљене поруке" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2495,6 +3734,12 @@ "value" : "Konfiguration av fördefinierade meddelanden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања унапред припремљених порука" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2554,6 +3799,12 @@ "value" : "M5 Stack Card KB / RAK Keypad" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "M5 стек картица KB / RAK тастатура" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2613,6 +3864,12 @@ "value" : "Manuell konfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ручна конфигурација" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2672,6 +3929,12 @@ "value" : "RAK Rotary Encoder-modul" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAK Rotary енкодер модул" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2687,10 +3950,30 @@ } }, "Carousel Interval" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал карусела" + } + } + } }, "Categories" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kategorien" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Категорије" + } + } + } }, "channel" : { "localizations" : { @@ -2736,6 +4019,12 @@ "value" : "Kanal" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2751,43 +4040,150 @@ } }, "Channel" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kanal" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал" + } + } + } }, "Channel 0 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 0 укључен" + } + } + } }, "Channel 1 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 1 укључен" + } + } + } }, "Channel 2 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 2 укључен" + } + } + } }, "Channel 3 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 3 укључен" + } + } + } }, "Channel 4 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 4 укључен" + } + } + } }, "Channel 5 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 5 укључен" + } + } + } }, "Channel 6 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 6 укључен" + } + } + } }, "Channel 7 Included" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канал 7 укључен" + } + } + } }, "channel details" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "детаљи канала" + } + } + } }, "Channel Name" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Назив канала" + } + } + } + }, + "Channel number must be between 0 and 7." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број канала мора бити између 0 и 7." + } + } + } }, "Channel Role" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улога канала" + } + } + } }, "Channel Utilization %@%% " : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искоришћеност канала %@%%" + } + } + } }, "channel.role.disabled" : { "extractionState" : "migrated", @@ -2834,6 +4230,12 @@ "value" : "Inaktiverad" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Онемогућено" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2893,6 +4295,12 @@ "value" : "Primär" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примарни" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2952,6 +4360,12 @@ "value" : "Sekundär" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Секундарни" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3010,6 +4424,12 @@ "value" : "Kanalutnyttjande" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искоришћеност канала" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3068,6 +4488,12 @@ "value" : "Kanaler" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канали" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3083,13 +4509,34 @@ } }, "Channels being added from the QR code did not save. When adding channels the names must be unique." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Канали који се додају из КР кода нису сачувани. Приликом додавања канала имена морају бити јединствена." + } + } + } }, "CHG" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CHG" + } + } + } }, "Clear" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очисти" + } + } + } }, "clear.app.data" : { "localizations" : { @@ -3135,6 +4582,12 @@ "value" : "Rensa appdata" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очисти податке апликације" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3193,6 +4646,12 @@ "value" : "Rensa" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очисти" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3208,19 +4667,54 @@ } }, "Client" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Клијент" + } + } + } }, "Client History" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Историја клијената" + } + } + } }, "Client History Request Sent" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за историју клијента је послат" + } + } + } }, "Client options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције клијента" + } + } + } }, "Clockwise Rotary Event" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ротациони догађај у смеру казаљке на сату" + } + } + } }, "close" : { "localizations" : { @@ -3266,6 +4760,12 @@ "value" : "Stäng" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Затвори" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3281,10 +4781,30 @@ } }, "Coding Rate" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стопа кодирања" + } + } + } }, "Color" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Farbe" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Боја" + } + } + } }, "communicating" : { "localizations" : { @@ -3330,6 +4850,12 @@ "value" : "Kommunicerar med enheten..." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Комуницирање са уређајем. ." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3382,6 +4908,12 @@ "value" : "När aktiverad räknar PAX-räknarmodulen antalet personer som passerar med WiFi och Bluetooth. Både WiFi och Bluetooth måste vara aktiverade för att PAX-räknaren ska fungera." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Када је омогућен, модул бројача пролазника броји број људи који пролазе користећи ВајФај и Блутут. И ВајФај и Блутут морају бити онемогућени да би бројач пролазника радио." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3434,6 +4966,12 @@ "value" : "PAX Räknare" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бројач пролазника" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3486,6 +5024,12 @@ "value" : "PAX Räknare Konfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања бројача пролазника" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3538,6 +5082,12 @@ "value" : "Uppdateringsintervall" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал ажурирања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3590,6 +5140,12 @@ "value" : "Hur ofta vi kan skicka ett meddelande till mesh-nätverket när personer upptäcks." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често можемо послати поруку мрежи када се открију људи." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3642,6 +5198,12 @@ "value" : "Multiplikator" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мултипликатор" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3694,6 +5256,12 @@ "value" : "ADC-överskrivning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преписивање ADC-а" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3711,12 +5279,6 @@ "config.power.ls.secs" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Light Sleep Interval" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -3747,6 +5309,12 @@ "value" : "Intervall för Ljussömn" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал благог спавања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3764,12 +5332,6 @@ "config.power.min.wake.secs" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Minimum Wake Interval" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -3800,6 +5362,12 @@ "value" : "Minsta Väckningsintervall" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минимални интервал будног стања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3819,7 +5387,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Power Saving" + "value" : "Stromsparen" } }, "en" : { @@ -3852,6 +5420,12 @@ "value" : "Strömsparläge" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уштеда енергије" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3868,12 +5442,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", @@ -3904,6 +5472,12 @@ "value" : "Sätter allt i viloläge så mycket som möjligt, för spårnings- och sensorläge kommer detta också inkludera LoRa-radion. Använd inte denna inställning om du vill använda din enhet med mobilappar eller använder en enhet utan en användarknapp." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Спаваће све што је више могуће, за улогу трагача и сензора ово ће укључивати и лора радио. Не користите ово подешавање ако желите да користите свој уређај са мобилним апликацијама или користите уређај без корисничког дугмета." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3923,7 +5497,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Battery" + "value" : "Batterie" } }, "en" : { @@ -3956,6 +5530,12 @@ "value" : "Batteri" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Батерија" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3976,7 +5556,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sleep" + "value" : "Schlafmodus" } }, "en" : { @@ -4009,6 +5589,12 @@ "value" : "Sömn" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стане спавања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4028,7 +5614,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Power" + "value" : "Strom" } }, "en" : { @@ -4061,6 +5647,12 @@ "value" : "Ström" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Снага" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4080,7 +5672,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "After" + "value" : "Nach" } }, "en" : { @@ -4113,6 +5705,12 @@ "value" : "Efter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Након" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4132,7 +5730,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Shutdown on Power Loss" + "value" : "Herunterfahren bei Stromunterbruch" } }, "en" : { @@ -4165,6 +5763,12 @@ "value" : "Stäng av vid Strömförlust" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искључи уређај при губитку напајања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4184,7 +5788,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Power Config" + "value" : "Stromkonfiguration" } }, "en" : { @@ -4217,6 +5821,12 @@ "value" : "Strömkonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања напајња" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4237,7 +5847,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth Off After" + "value" : "Bluetooth Aus nach" } }, "en" : { @@ -4270,6 +5880,12 @@ "value" : "Bluetooth Stängs Av Efter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут се искључује након" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4290,7 +5906,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "RTTTL Ringtone" + "value" : "RTTTL Klingelton" } }, "en" : { @@ -4323,6 +5939,12 @@ "value" : "RTTTL Ringsignal" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "RTTTL мелодија звона" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4339,12 +5961,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", @@ -4375,6 +5991,12 @@ "value" : "Ringsignalöverföringsspråk (RTTTL) Ringsignalsträng som används av stödda buzzers i externa notifikationer." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Језик преноса мелдоије звона (RTTTL) Стринг мелодије звона који користе подржани звучни сигнали у спољним обавештењима." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4391,12 +6013,6 @@ }, "config.ringtone.label" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ringtone Transfer Language" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -4427,6 +6043,12 @@ "value" : "Språk för Överföring av Ringsignal" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Језик преноса мелодије звона" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4446,7 +6068,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ringtone Config" + "value" : "Klingelton Konfiguration" } }, "en" : { @@ -4479,6 +6101,12 @@ "value" : "Ringsignalskonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација звона" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4498,7 +6126,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" : { @@ -4537,6 +6165,12 @@ "value" : "Efter att konfigurationsvärdena sparats kommer noden att starta om." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Након што сачувате вредности конфигурације, чвор ће се поново покренути." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4552,19 +6186,56 @@ } }, "Configuration for: %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација за: %@" + } + } + } }, "Configuration Presets" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Унапред подешене конфигурације" + } + } + } }, "Configure" : { - - }, - "Configuring Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurieren" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигуриши" + } + } + } }, "Connect to a Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbunden mit einem Knoten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повежите се са чвором" + } + } + } }, "connected" : { "localizations" : { @@ -4610,6 +6281,12 @@ "value" : "Bluetooth Ansluten" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блутут повезан" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4625,7 +6302,20 @@ } }, "Connected Node %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbunden mit Knoten %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезани чвор %@" + } + } + } }, "connected.radio" : { "localizations" : { @@ -4671,6 +6361,12 @@ "value" : "Ansluten Radio" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезани радио" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4729,6 +6425,12 @@ "value" : "Ansluter..." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезујем се . ." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4744,7 +6446,20 @@ } }, "Connection Attempt %lld of 10" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindungsversuch %lld von 10" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Покушај повезивања %lld од 10" + } + } + } }, "contacts" : { "extractionState" : "manual", @@ -4791,6 +6506,12 @@ "value" : "Kontakter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Контакти" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4804,9 +6525,6 @@ } } } - }, - "Contacts" : { - }, "contacts %@" : { "extractionState" : "migrated", @@ -4853,6 +6571,12 @@ "value" : "Kontakter (%@)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Контакти (%@)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4868,33 +6592,91 @@ } }, "Control Type" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тип контроле" + } + } + } }, "Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Контролише трептајући ЛЕД на уређају. За већину уређаја ово ће контролисати један од до максималних 4 ЛЕД, ЛЕД пуњења и ГПС ЛЕД диоде се не могу контролисати." + } + } + } }, "Convex Hull" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konvexe Hülle" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конвексна љуштура" + } + } + } }, "Coordinate" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koordinate" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Координате" + } + } + } }, "Coordinate %@, %@" : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koordinate %1$@, %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Coordinate %1$@, %2$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Координате %1$@, %2$@" + } } } }, - "Coordinates: %@, %@" : { + "Coordinates:" : { "localizations" : { - "en" : { + "de" : { "stringUnit" : { - "state" : "new", - "value" : "Coordinates: %1$@, %2$@" + "state" : "translated", + "value" : "Koordinaten:" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Координате:" } } } @@ -4943,6 +6725,12 @@ "value" : "Kopiera" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копирај" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4957,14 +6745,63 @@ } } }, + "Could not find node" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten nicht gefunden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није могуће наћи чвор" + } + } + } + }, "Counter Clockwise Rotary Event" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ротациони догађај у смеру супротном од казаљке на сату" + } + } + } }, "Create Waypoint" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wegpunkt erstellen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Креирајте путну тачку" + } + } + } }, "Created: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erstellt: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Креирано : %@" + } + } + } }, "current" : { "extractionState" : "stale", @@ -4972,7 +6809,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Current" + "value" : "Aktuell" } }, "en" : { @@ -5011,6 +6848,12 @@ "value" : "Aktuell" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутни" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5026,38 +6869,125 @@ } }, "Current Firmware Version: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelle Firmware Version: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутна верзија фирмвера: %@" + } + } + } }, "Current Firmware Version: %@, Latest Firmware Version: %@" : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuelle Firmware Version: %1$@, neuste Firmware Version %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Current Firmware Version: %1$@, Latest Firmware Version: %2$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутна верзија фирмвера: %1$@, најновија верзија фирмвера: %2$@" + } } } }, "Current: %lld" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktuell: %lld" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутно: %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." : { - + "extractionState" : "stale", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутно препоручени начин за ажурирање ЕСП32 уређаја је коришћење веб флешера на десктоп рачунару из прегледача заснованог на хрому. Не ради на мобилним уређајима или преко BLE-а." + } + } + } + }, + "Currently the recommended 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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутно препоручени начин за ажурирање ЕСП32 уређаја је коришћење веб флешера на десктоп рачунару из прегледача заснованог на хрому. Не ради на мобилним уређајима или преко BLE-а." + } + } + } }, "Date" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datum" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Датум" + } + } + } }, "Debug" : { - - }, - "Debug Log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дебагуј" + } + } + } }, "Debug Logs" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дебаг логови" + } + } + } }, "Debug Logs%@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debug логови%@" + } + } + } }, "default" : { "extractionState" : "migrated", @@ -5104,6 +7034,12 @@ "value" : "Standard" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подразумевано" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5119,7 +7055,37 @@ } }, "Default" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standard" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подразумевано" + } + } + } + }, + "default.128x64.screen.layout" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Default 128x64 screen layout" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подразумевани изглед екрана 128x64" + } + } + } }, "delete" : { "localizations" : { @@ -5165,6 +7131,12 @@ "value" : "Ta bort" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обриши" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5180,40 +7152,146 @@ } }, "Delete all environment metrics?" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Желите ли да избришете све показатеље окружења?" + } + } + } }, "Delete all map tiles?" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избрисати све плочице мапе?" + } + } + } }, "Delete all positions?" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избрисати све позиције?" + } + } + } }, "Delete Message" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обриши поруку" + } + } + } }, "Delete Messages" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обриши поруке" + } + } + } }, "Delete Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten löschen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обриши чвор" + } + } + } + }, + "Delete Node?" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten löschen?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обрисати чвор?" + } + } + } }, "Description" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опис" + } + } + } + }, + "Description must be less than 100 bytes" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опис мора бити испод 100 бајтова" + } + } + } }, "Detection" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откривање" + } + } + } }, "Detection event" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Догађај откривања" + } + } + } }, "Detection Sensor Log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови сензора откривања" + } + } + } }, "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" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поруке сензора за откривање се примају као текстуалне поруке. Ако омогућите обавештења, добићете обавештење за сваку примљену поруку за откривање и одговарајућу значку непрочитане поруке." + } + } + } }, "detection.sensor" : { "localizations" : { @@ -5259,6 +7337,12 @@ "value" : "Detektionssensor" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сензор откривања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5304,6 +7388,12 @@ "state" : "translated", "value" : "Konfiguration av Detektionssensor" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања ензора откривања" + } } } }, @@ -5338,11 +7428,24 @@ "state" : "translated", "value" : "Logg för Detektionssensor" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови сензора откривања" + } } } }, "Developers" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Програмери" + } + } + } }, "device" : { "localizations" : { @@ -5388,6 +7491,12 @@ "value" : "Enhet" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уређај" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5403,25 +7512,86 @@ } }, "Device GPS" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geräte-GPS" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS уређај" + } + } + } }, - "Device Logging Enabled" : { - + "Device is managed by a mesh administrator, the user is unable to access any of the device settings." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уређајем управља администратор мреже, корисник не може да приступи ниједном подешавању уређаја." + } + } + } }, "Device Metrics" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Метрика уређаја" + } + } + } }, "Device Metrics Log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови метрике уређаја" + } + } + } }, "Device Model: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerätemodell: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Модел уређаја: %@" + } + } + } }, "Device Role" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улога уређаја" + } + } + } }, "Device Screen" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екран уређаја" + } + } + } }, "device.config" : { "localizations" : { @@ -5467,6 +7637,12 @@ "value" : "Enhetskonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања уређаја" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5486,7 +7662,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Device Configuration" + "value" : "Gerätekonfiguration" } }, "en" : { @@ -5519,6 +7695,12 @@ "value" : "Enhetsinställningar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања уређаја" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5535,12 +7717,6 @@ }, "device.metrics.delete" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete all device metrics?" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -5577,6 +7753,12 @@ "value" : "Ta bort alla enhetsmätvärden?" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избришите све метрике уређаја?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5635,6 +7817,12 @@ "value" : "Logg för Enhetsmätvärden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови метрике уређаја" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5694,6 +7882,12 @@ "value" : "Appansluten eller fristående meddelandeenhet." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Апликација повезана или самостални уређај за размену порука." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5753,6 +7947,12 @@ "value" : "Enhet som endast sänder ut när det behövs för stealth eller energibesparing." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уређај који емитује само по потреби ради прикривености или уштеде енергије." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5773,7 +7973,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" : { @@ -5812,6 +8012,12 @@ "value" : "Enhet som inte vidarebefordrar paket från andra enheter." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уређај који не прослеђује пакете примљене од других уређаја." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5871,6 +8077,12 @@ "value" : "Sänder regelbundet ut plats som meddelande till standardkanalen för att underlätta återhämtning av enheten." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Редовно емитује локацију као поруку подразумеваном каналу ради помоћи при проналаску уређаја." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5885,60 +8097,253 @@ } } }, + "device.role.name.client" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Client" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Клијент" + } + } + } + }, + "device.role.name.clientHidden" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Client Hidden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скривени клијент" + } + } + } + }, + "device.role.name.clientMute" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Client Mute" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Клијент мутиран" + } + } + } + }, + "device.role.name.lostAndFound" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lost and Found" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изгубљено и нађено" + } + } + } + }, + "device.role.name.repeater" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repeater" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поновљач" + } + } + } + }, + "device.role.name.router" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Router" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рутер" + } + } + } + }, + "device.role.name.routerClient" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Router & Client" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рутер и клијент" + } + } + } + }, + "device.role.name.sensor" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensor" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сензор" + } + } + } + }, + "device.role.name.tak" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAK" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAK" + } + } + } + }, + "device.role.name.takTracker" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAK Tracker" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ТАК Трекер" + } + } + } + }, + "device.role.name.tracker" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tracker" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Трекер" + } + } + } + }, "device.role.repeater" : { "extractionState" : "migrated", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "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." + "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" : { "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." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Инфраструктурни чвор само на торњу или врху планине. Није намењен за кровове или мобилне чворове. Прослеђује поруке уз минимално оптерећење. Није видљив у листи чворова." + } + }, "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 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。" } } @@ -5950,54 +8355,60 @@ "de" : { "stringUnit" : { "state" : "translated", - "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." + "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" : { "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." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Инфраструктурни чвор само на торњу или врху планине. Не користи се за кровове или мобилне чворове. Потребна му је изузетна покривеност. Видљиво на листи чворова." + } + }, "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 將無法連接到電台進行收發操作。" } } @@ -6008,14 +8419,14 @@ "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" : { "stringUnit" : { "state" : "translated", - "value" : "Combination of both ROUTER and CLIENT. Not for mobile devices. Deprecated" + "value" : "Deprecated role use client." } }, "fr" : { @@ -6048,6 +8459,12 @@ "value" : "Kombination av både ROUTER och CLIENT. Inte för mobila enheter." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Застарело. Користи клијент ролу." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "needs_review", @@ -6107,6 +8524,12 @@ "value" : "Sänder ut telemetripaket som prioritet." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Емитује телеметријске пакете као приоритет." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6166,6 +8589,12 @@ "value" : "Optimerad för kommunikation med ATAK-systemet, minskar rutinutsändningar." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оптимизован за комуникацију са ATAK системом, смањује рутинске емисије." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6225,6 +8654,12 @@ "value" : "Aktiverar automatiska TAK PLI-utsändningar och minskar rutinutsändningar." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава аутоматске TAK PLI емисије и смањује рутинске емисије." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6284,6 +8719,12 @@ "value" : "Sänder ut GPS-positionspaket som prioritet." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Емитује пакете са GPS позицијом као приоритет." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6299,10 +8740,60 @@ } }, "Dilution of precision (DOP) PDOP used by default" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разређење прецизности (DOP) PDOP се користи као подразумевано" + } + } + } }, "Direct" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Direkt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Директно" + } + } + } + }, + "Direct Message Help" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помоћ за директне поруке" + } + } + } + }, + "Direct messages are using the new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Директне поруке користе нову инфраструктуру јавних кључева за енкрипцију. Захтева верзију фирмвера 2.5 или новију." + } + } + } + }, + "Direct messages are using the shared key for the channel." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Директне поруке користе дељени кључ за канал." + } + } + } }, "direct.messages" : { "localizations" : { @@ -6348,6 +8839,12 @@ "value" : "Direktmeddelanden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Директне поруке" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6363,7 +8860,20 @@ } }, "Disabled" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschaltet" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Онемогућено" + } + } + } }, "disconnect" : { "localizations" : { @@ -6409,6 +8919,12 @@ "value" : "Koppla från" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прекините везу" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6428,7 +8944,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dismiss Keyboard" + "value" : "Tastatur ausblenden" } }, "en" : { @@ -6467,6 +8983,12 @@ "value" : "Stäng" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отпусти" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6525,6 +9047,12 @@ "value" : "Skärm" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приказ" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6540,22 +9068,37 @@ } }, "Display Fahrenheit" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приказ фаренхајта" + } + } + } }, "Display Mode" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Режим приказа" + } + } + } }, "Display Units" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јединице приказа" + } + } + } }, "display.config" : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Display Config" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -6592,6 +9135,12 @@ "value" : "Skärmkonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања приказа" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6651,6 +9200,12 @@ "value" : "Distans" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Раздаљина" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6666,28 +9221,90 @@ } }, "Distance" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distanz" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Раздаљина" + } + } + } }, "Documentation" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Документација" + } + } + } }, "Double Tap as Button" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Двоструки додир као дугме" + } + } + } }, "Downlink Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дозвољен даунлинк" + } + } + } }, "Drag & Drop Firmware Update" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажурирање фирмвера методом превуци-и-испусти" + } + } + } }, "Drag & Drop Firmware Update Documentation" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Документација ажурирања фирмвера методом превуци-и-испусти" + } + } + } }, "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." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Превуци-и-испусти је препоручен начин за ажурирање фирмвера на NRF уређајима. Ако ваш iPhone или iPad има USB-C, радиће са вашим уобичајеним USB-C каблом за пуњење. За уређаје са Lightning портом потребан је Apple Lightning to USB адаптер за камеру." + } + } + } }, - "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." : { - + "Drop Pin in Maps" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Постави ознаку на мапама" + } + } + } }, "echo" : { "localizations" : { @@ -6733,6 +9350,12 @@ "value" : "Eko" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ехо" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6748,10 +9371,24 @@ } }, "Editing Waypoint" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уређивање путне тачке" + } + } + } }, "Elev. Gain" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повећање надморске висине" + } + } + } }, "email.address" : { "extractionState" : "manual", @@ -6798,6 +9435,12 @@ "value" : "E-postadress" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Имејл адреса" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6812,17 +9455,35 @@ } } }, - "Empty" : { - + "Emoji" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Емоџи" + } + } + } }, - "Enable MB Tiles" : { - + "Empty" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Празно" + } + } + } }, "Enable Notifications" : { - - }, - "Enable Offline Maps" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогући обавештења" + } + } + } }, "enabled" : { "localizations" : { @@ -6868,6 +9529,12 @@ "value" : "Aktiverad" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућено" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6883,25 +9550,64 @@ } }, "Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава уређајима са изворним I2S аудио излазом да користе РТТТЛ преко звучника као звучник. Т-Ватцх СКСНУМКС и Т-Децк на пример имају ову могућност." + } + } + } }, "Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава модул сензора детекције. Потребно је да буде омогућен и на чвору са сензором, и на свим чворовима које желите да примате текстуалне поруке сензора детекције или да видите дневник и графикон сензора детекције." + } + } + } }, "Enables the store and forward module. Store and forward must be enabled on both client and router devices." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава модул за чување и пренос. Чување и пренос мора бити омогућено на оба уређаја, клијенту и рутеру." + } + } + } }, "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." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућавање етернета ће онемогућити блутут везу са апликацијом." + } + } + } }, "Enabling WiFi will disable the bluetooth connection to the app." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућавање ВајФаја ће онемогућити блутут везу са апликацијом." + } + } + } }, "Encoder Press Event" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Догађај притиска енкодера" + } + } + } }, "encrypted" : { "localizations" : { @@ -6947,6 +9653,12 @@ "value" : "Krypterad" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шифровано" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6961,47 +9673,195 @@ } } }, + "Encrypted" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschlüsselt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шифровано" + } + } + } + }, "Encryption Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућено шифровање" + } + } + } }, "Enter DFU Mode" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DFÜ-Modus aktivieren" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уђите у DFU режим" + } + } + } }, "environment" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "окружење" + } + } + } }, "Environment" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umgebung" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Окружење" + } + } + } }, "Environment Metrics Log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дневник метрика окружења" + } + } + } }, "Erase all app data?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle App-Daten löschen?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избрисати све податке апликације?" + } + } + } }, "Erase all device and app data?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Geräte- und App-Daten löschen?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избрисати све податке уређаја и апликације?" + } + } + } + }, + "Error: %@" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Грешка: %@" + } + } + } }, "ESP 32 OTA update is a work in progress, click the button below to send your device a reboot into ota admin message." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ESP32 OTA ажурирање је у развоју, кликните на дугме испод да бисте послали уређају поруку за поновно покретање у OTA администраторски режим." + } + } + } }, "ESP32 Device Firmware Update" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажурирање фирмвера за ESP32 уређај" + } + } + } }, "Ethernet Options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Етернет опције" + } + } + } }, "Exchange Positions" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Размени локације" + } + } + } }, "Expire" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Истиче" + } + } + } }, "Expires" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Истиче" + } + } + } }, "Expires: %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Истиче: %@" + } + } + } }, "export" : { "localizations" : { @@ -7047,6 +9907,12 @@ "value" : "Export" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Извоз" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7105,6 +9971,12 @@ "value" : "Extern Notifikation" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Спољна обавештења" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7163,6 +10035,12 @@ "value" : "Konfiguration av Extern Notifikation" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавање спољних обавештења" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7178,35 +10056,183 @@ } }, "Factory Reset" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Werkseinstellungen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ресетовање на фабричка подешавања" + } + } + } }, "Factory reset your device and app? " : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerät und App auf Werkseinstellungen zurücksetzen?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вратите уређај и апликацију на фабричка подешавања?" + } + } + } + }, + "Failed to encode message content" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспело кодирање садржаја поруке" + } + } + } + }, + "Failed to get a valid position to exchange" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добијање важеће позиције за размену није успело" + } + } + } + }, + "Failed to get a valid position to exchange." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добијање важеће позиције за размену није успело." + } + } + } }, "Favorite" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омиљени" + } + } + } }, "Favorites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омиљени" + } + } + } + }, + "Favorites and nodes with recent messages show up at the top of the contact list." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омиљени чворови и чворови са недавно примљеним порукама појављују се на врху листе контаката." + } + } + } + }, + "Fetch the latest position of a cetain node" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Letzte Position eines Knotens holen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преузмите најновију позицију одређеног чвора" + } + } + } }, "Fifteen Minutes" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Петнаест минута" + } + } + } }, "File Storage" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Складиште података" + } + } + } }, "Find a contact" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontakt suchen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пронађи контакт" + } + } + } }, "Find a node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Knoten finden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пронађи чвор" + } + } + } }, "finish" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Finish" + "value" : "Beenden" } }, "en" : { @@ -7245,6 +10271,12 @@ "value" : "Avsluta" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Заврши" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7260,13 +10292,46 @@ } }, "Firmware" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmware" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фирмвер" + } + } + } }, "Firmware update docs" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Документи за ажурирање фирмвера" + } + } + } }, "Firmware Updates" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmwareaktualisierungen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажурирања фирмвера" + } + } + } }, "firmware.version" : { "localizations" : { @@ -7312,6 +10377,12 @@ "value" : "Firmwareversion" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Верзија фирмвера" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7371,6 +10442,12 @@ "value" : "Okänd Firmwareversion upptäckt, kan inte ansluta till enheten." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откривена је неподржана верзија фирмвера, није могуће повезати са уређајем." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7386,43 +10463,158 @@ } }, "First heard" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прво откривање" + } + } + } }, "Five Minutes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fünf Minuten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пет минута" + } + } + } }, "Fixed Position" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фиксна локација" + } + } + } }, "Flip Screen" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Окрени екран" + } + } + } }, "Flip screen vertically" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Окрени екран вертикално" + } + } + } }, "For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "За сву MQTT функционалност осим извештаја на мапи, такође морате подесити uplink и downlink за сваки канал који желите да прележете преко MQTT-а.”" + } + } + } }, "For everyone" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Für alle" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "За све" + } + } + } }, "For me" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Für mich" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "За мене" + } + } + } }, "Frequency" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frequenz" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фреквенција" + } + } + } }, "Frequency Override" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Измена фреквенције" + } + } + } }, "Frequency Slot" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фреквенцијски слот" + } + } + } }, "Friendly name" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пријатељски назив" + } + } + } }, "Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пријатељски назив који се користи за форматирање поруке послате на мрежу. На пример: Назив „Motion” довео би до поруке „Motion detected”." + } + } + } }, "gas" : { "extractionState" : "manual", @@ -7469,6 +10661,12 @@ "value" : "Gas" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Гас" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7486,12 +10684,6 @@ "gas.resistance" : { "extractionState" : "manual", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gas Resistance" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -7528,6 +10720,12 @@ "value" : "Gasmotstånd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отпорност на гас" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7586,6 +10784,12 @@ "value" : "Generera QR-kod" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Генерисање QR кода" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7601,46 +10805,160 @@ } }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Набавите прилагођене водоотпорне соларне и детекционе сензорске рутер чворове, алуминијумске десктоп чворове и издржљиве мобилне уређаје." + } + } + } + }, + "Get Node Position" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotenposition ermitteln" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добави позицију чвора" + } + } + } }, "Get NRF DFU from the App Store" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преузмите NRF DFU из App Store-а" + } + } + } }, "Get the latest alpha firmware" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добави најновији алфа фирмвер" + } + } + } }, "Get the latest stable firmware" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добави најновији стабилни фирмвер" + } + } + } }, "GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO" + } + } + } }, "GPIO Output Duration" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Трајање GPIO излаза" + } + } + } }, "GPIO pin for rotary encoder A port." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO пин за A порт ротационог енкодера." + } + } + } }, "GPIO pin for rotary encoder B port." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO пин за Б порт ротационог енкодера." + } + } + } }, "GPIO pin for rotary encoder Press port." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO пин за порт клика ротационог енкодера." + } + } + } }, "GPIO Pin to monitor" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO пин за надгледање" + } + } + } }, "GPS EN GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS EN GPIO" + } + } + } }, "GPS Format" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS формат" + } + } + } }, "GPS Receive GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS пријем GPIO" + } + } + } }, "GPS Transmit GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS предаја GPIO" + } + } + } }, "gpsformat.dec" : { "extractionState" : "migrated", @@ -7687,6 +11005,12 @@ "value" : "Decimalgrader" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Формат децималних степени" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7746,6 +11070,12 @@ "value" : "Grader Minuter Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Степени Минути Секунде" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7805,6 +11135,12 @@ "value" : "Militärt rutnätsreferenssystem" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Војни референтни систем мреже" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7864,6 +11200,12 @@ "value" : "Öppen Platskod (även känd som Pluskoder)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отворени код локације (тј. Плус кодови)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7923,6 +11265,12 @@ "value" : "Ordnance Survey Rutnätsreferens" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Референца мреже Орданс Сурвеја" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7982,6 +11330,12 @@ "value" : "Universal Transversal Mercator" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Универзални трансверзални Меркаторов пројекат" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -7999,6 +11353,12 @@ "gpsmode.disabled" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeschaltet" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -8028,12 +11388,24 @@ "state" : "translated", "value" : "Inaktiverad" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Онемогућен" + } } } }, "gpsmode.enabled" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eingeschaltet" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -8063,6 +11435,12 @@ "state" : "translated", "value" : "Aktiverad" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућен" + } } } }, @@ -8098,20 +11476,70 @@ "state" : "translated", "value" : "Inte närvarande" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није пристуно" + } + } + } + }, + "Group Message" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppennachricht" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Групна порука" + } } } }, "Gusts %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јаки удари ветра %@" + } + } + } }, "Hardware" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хардвер" + } + } + } }, "Heading" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Смер" + } + } + } }, "Heading: %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Смер: %@" + } + } + } }, "heard" : { "extractionState" : "migrated", @@ -8158,6 +11586,12 @@ "value" : "Hörd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чуо" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8217,6 +11651,12 @@ "value" : "Senast Hörd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прво откривање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8232,67 +11672,278 @@ } }, "Help with App Development" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помози при развоју апликације" + } + } + } }, "Hide alerts" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сакриј упозорења" + } + } + } }, "Hide Alerts" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сакриј алертове" + } + } + } }, "HIGH" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "HOCH" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ВИСОК" + } + } + } }, "History Return Max" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимални повратак историје" + } + } + } }, "History Return Window" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временски прозор поврата историје" + } + } + } }, "Hops Away" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скокови удаљености" + } + } + } }, - "Hops Away %d) dB" : { - + "Hops Away %d" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt %d" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удаљено %d скокова" + } + } + } }, "Hops Away:" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt:" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скокови удаљености:" + } + } + } + }, + "Hops Away: %d" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hops Entfernt: %d" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скокови удаљености: %d" + } + } + } }, "Hour" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stunde" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сат" + } + } + } }, "Hourly Duty Cycle" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Натпросечни циклус дужности по сату" + } + } + } }, "How long the screen remains on after the user button is pressed or messages are received." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико дуго екран остаје укључен након притиска корисничког дугмета или пријема порука." + } + } + } }, "How often device metrics are sent out over the mesh. Default is 30 minutes." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често се метрике уређаја шаљу преко мреже. Подразумевано је 30 минута." + } + } + } }, "How often power metrics are sent out over the mesh. Default is 30 minutes." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често се метрике снаге шаљу преко мреже. Подразумевано је 30 минута." + } + } + } }, "How often sensor metrics are sent out over the mesh. Default is 30 minutes." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често се метрике сензора шаљу преко мреже. Подразумевано је 30 минута." + } + } + } }, "How often should we try to get a GPS position." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често треба да покушамо да добијемо GPS позицију." + } + } + } }, "How often to send detection sensor state to mesh regardless of detection. Default is Never." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често да пошаљете стање сензора детекције у мрежу, без обзира на детекцију. Подразумевано је да се не шаље никада." + } + } + } }, "How to update Firmware" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wie wird die Firmware aktualisiert" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Како да ажурираш фирмвер" + } + } + } }, "Hum" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влажност" + } + } + } }, "Humidity" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luftfeuchtigkeit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влажност" + } + } + } }, "HUMIDITY" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "LUFTFEUCHTIGKEIT" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ВЛАЖНОСТ" + } + } + } }, "hybrid" : { "extractionState" : "migrated", @@ -8339,6 +11990,12 @@ "value" : "Hybrid" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хибридни" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8398,6 +12055,12 @@ "value" : "Hybrid Flygöversikt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хибридни надлет" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8413,37 +12076,134 @@ } }, "IAQ" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + } + } }, "IAQ " : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + } + } }, "IAQ %lld" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ %lld" + } + } + } }, "Icon" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Иконица" + } + } + } }, "If DOP is set, use HDOP / VDOP values instead of PDOP" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ако је DOP постављен, користите HDOP / VDOP вредности уместо PDOP-а" + } + } + } }, "If enabled, the 'output' Pin will be pulled active high, disabled means active low." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ако је омогућено, 'output' пин ће бити активиран на високом нивоу, а ако је онемогућено, биће активиран на ниском нивоу." + } + } + } }, "If it is hard to access your device's reset button enter DFU mode here." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ако је тешко приступити дугмету за ресетовање уређаја, уђите у DFU режим овде." + } + } + } }, "If set, any packets you send will be echoed back to your device." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ако је подешено, сви пакети које пошаљете ће бити враћени (ехо) назад на ваш уређај." + } + } + } }, "If the default region topic is too busy you can choose a more local topic." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ако је подразумевана тема региона превише заузета можете изабрати више локалну тему." + } + } + } }, "Ignore MQTT" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнориши MQTT" + } + } + } + }, + "Ignore Node" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнориши чвор" + } + } + } + }, + "Ignored" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнорисан" + } + } + } }, "Import Route" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увозна рута" + } + } + } }, "include" : { "localizations" : { @@ -8489,6 +12249,12 @@ "value" : "Inkludera" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укључите" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8504,12 +12270,12 @@ } }, "incomplete" : { - "extractionState" : "migrated", + "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Incomplete" + "value" : "Unvollständig" } }, "en" : { @@ -8542,6 +12308,12 @@ "value" : "Incomplete" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Недовршен" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8557,10 +12329,24 @@ } }, "Indoor Air Quality" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Квалитет ваздуха у затвореном простору" + } + } + } }, "Indoor Air Quality (IAQ)" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Квалитет ваздуха у затвореном простору (IAQ)" + } + } + } }, "inputevent.back" : { "extractionState" : "migrated", @@ -8607,6 +12393,12 @@ "value" : "Bakåt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Назад" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8666,6 +12458,12 @@ "value" : "Avbryt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откажи" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8725,6 +12523,12 @@ "value" : "Ner" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Доле" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8784,6 +12588,12 @@ "value" : "Vänster" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лево" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8843,6 +12653,12 @@ "value" : "Ingen" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ништа" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8902,6 +12718,12 @@ "value" : "Höger" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Десно" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -8961,6 +12783,12 @@ "value" : "Välj" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изабери" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9020,6 +12848,12 @@ "value" : "Upp" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Горе" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9035,7 +12869,14 @@ } }, "Inputs" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улази" + } + } + } }, "interval.eighteen.hours" : { "extractionState" : "migrated", @@ -9043,7 +12884,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Eighteen Hours" + "value" : "Achtzehn Stunden" } }, "en" : { @@ -9082,6 +12923,12 @@ "value" : "Arton Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Осамнаест сати" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9099,6 +12946,12 @@ "interval.eventytwo.hours" : { "extractionState" : "manual", "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Двадесет и два сата" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9119,7 +12972,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fünfzehn Minutes" + "value" : "Fünfzehn Minuten" } }, "en" : { @@ -9158,6 +13011,12 @@ "value" : "Femton Minuter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Петнаест минута" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9217,6 +13076,12 @@ "value" : "Femton Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Петнаест секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9237,7 +13102,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Five Hours" + "value" : "Fünf Stunden" } }, "en" : { @@ -9276,6 +13141,12 @@ "value" : "Fem Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пет сати" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9296,7 +13167,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fünf Minutes" + "value" : "Fünf Minuten" } }, "en" : { @@ -9335,6 +13206,12 @@ "value" : "Fem Minuter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пет минута" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9394,6 +13271,12 @@ "value" : "Fem Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пет секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9414,7 +13297,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Forty Eight Hours Hours" + "value" : "Achtundvierzig Stunden" } }, "en" : { @@ -9452,6 +13335,12 @@ "state" : "translated", "value" : "Fyrtioåtta Timmar" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Четртесет и осам сати" + } } } }, @@ -9461,7 +13350,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Forty Five Seconds" + "value" : "Fündundvierzig Sekunden" } }, "en" : { @@ -9500,6 +13389,12 @@ "value" : "Fyrtiofem Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Четрдесет и пет секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9520,7 +13415,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Four Hours" + "value" : "Vier Stunden" } }, "en" : { @@ -9559,6 +13454,12 @@ "value" : "Fyra Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Четири сата" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9579,7 +13480,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Four Seconds" + "value" : "Vier Sekunden" } }, "en" : { @@ -9618,6 +13519,12 @@ "value" : "Fyra Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Четири секунде" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9677,6 +13584,12 @@ "value" : "En Timme" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Један сат" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9736,6 +13649,12 @@ "value" : "En Minut" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Један минут" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9795,6 +13714,12 @@ "value" : "En Sekund" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Један секунд" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9815,7 +13740,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Seventy Two Hours" + "value" : "Zweiundsiebzig Stunden" } }, "en" : { @@ -9853,6 +13778,12 @@ "state" : "translated", "value" : "Sjuttiotvå Timmar" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Седамдесет и два сата" + } } } }, @@ -9901,6 +13832,12 @@ "value" : "Sex Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шест сати" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -9921,7 +13858,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Zehn Minutes" + "value" : "Zehn Minuten" } }, "en" : { @@ -9960,6 +13897,12 @@ "value" : "Tio Minuter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Десет минута" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10019,6 +13962,12 @@ "value" : "Tio Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Десет секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10039,7 +13988,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dreißig Minutes" + "value" : "Dreißig Minuten" } }, "en" : { @@ -10078,6 +14027,12 @@ "value" : "Trettio Minuter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пола сата" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10137,6 +14092,12 @@ "value" : "Trettio Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тридесет секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10157,7 +14118,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Thirty Six Hours" + "value" : "Sechsunddreissig Stunden" } }, "en" : { @@ -10196,6 +14157,12 @@ "value" : "Trettiosex Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тридесет и шест сати" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10216,7 +14183,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Three Hours" + "value" : "Drei Stunden" } }, "en" : { @@ -10255,6 +14222,12 @@ "value" : "Tre Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Три сата" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10275,7 +14248,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Three Seconds" + "value" : "Drei Sekunden" } }, "en" : { @@ -10314,6 +14287,12 @@ "value" : "Tre Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Три секунде" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10373,6 +14352,12 @@ "value" : "Tolv Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дванаест сати" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10432,6 +14417,12 @@ "value" : "Tjugo Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Двадесет секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10491,6 +14482,12 @@ "value" : "Tjugofem Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Двадесет пет секунди" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10550,6 +14547,12 @@ "value" : "Tjugofyra Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Двадесет четири сата" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10570,7 +14573,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Two Hours" + "value" : "Zwei Stunden" } }, "en" : { @@ -10609,6 +14612,12 @@ "value" : "Två Timmar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Два сата" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10668,6 +14677,12 @@ "value" : "Två Minuter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Два минута" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10727,6 +14742,12 @@ "value" : "Två Sekunder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Две секунде" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10741,37 +14762,84 @@ } } }, - "interval.tyeight.hours" : { + "inverted.top.bar.for.2.color.display" : { "extractionState" : "manual", "localizations" : { - "zh-Hans" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "四十八小时小时" + "value" : "Inverted top bar for 2 Color display" } }, - "zh-Hant-TW" : { + "sr" : { "stringUnit" : { "state" : "translated", - "value" : "四十八小时小時" + "value" : "Обрнута горња трака за екран у 2 боје" } } } }, "JSON Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON омогућен" + } + } + } }, "JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON режим је ограничен, нешифрован MQTT излаз за локалну интеграцију са Home Assistant-ом." + } + } + } }, "Key" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taste" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кључ" + } + } + } }, "Key Mapping" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мапирање кључева" + } + } + } }, "Key Size" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tastengröße" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Величина кључа" + } + } + } }, "keyboard.type" : { "extractionState" : "manual", @@ -10818,6 +14886,12 @@ "value" : "Tangentbordstyp" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тип тастатуре" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10833,41 +14907,171 @@ } }, "Last heard" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zuletzt gehört" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Последње откривање" + } + } + } }, "Latitude" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Breitengrad" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ширина" + } + } + } }, "LED Heartbeat" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LED срчани откуцаји" + } + } + } }, "LED State" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LED статус" + } + } + } + }, + "Legacy Administration" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стари начин администрације" + } + } + } }, "Licensed Operator" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лиценцирани оператор" + } + } + } }, "Limit all periodic broadcast intervals especially telemetry and position. If you need to increase hops, do it on nodes at the edges, not the ones in the middle. MQTT is not advised when you are duty cycle restricted because the gateway node is then doing all the work." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ограничите све периодичне интервале емитовања, посебно телеметрију и позицију. Ако је потребно повећати број скокова, то радите на чворовима на ивицама, а не на оним у средини. MQTT није препоручен када сте ограничени циклусом дужности јер у том случају чвор-рутер ради сав посао." + } + } + } }, "Line Series" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Линијска серија" + } + } + } }, - "Location: %@" : { - + "Loading Logs. . ." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Учитавам логове. . ." + } + } + } + }, + "Location" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standort" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Локација:" + } + } + } + }, + "Location:" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Standrot:" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Локација:" + } + } + } }, "Locked" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesperrt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закључан" + } + } + } }, "Log Levels" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нивои логова" + } + } + } }, "log.category" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Category" + "value" : "Kategorie" } }, "en" : { @@ -10900,6 +15104,12 @@ "value" : "Category" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Категорија" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10952,6 +15162,12 @@ "value" : "Level" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ниво" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -10971,7 +15187,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Message" + "value" : "Nachricht" } }, "en" : { @@ -11004,6 +15220,12 @@ "value" : "Message" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Порука" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11024,7 +15246,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Process" + "value" : "Prozess" } }, "en" : { @@ -11057,6 +15279,12 @@ "value" : "Process" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Процес" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11110,6 +15338,12 @@ "value" : "Subsystem" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подсистем" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11129,7 +15363,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Time" + "value" : "Zeit" } }, "en" : { @@ -11162,6 +15396,12 @@ "value" : "Time" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11220,6 +15460,12 @@ "value" : "Loggning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логовање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11235,19 +15481,82 @@ } }, "Logs" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови" + } + } + } }, "Logs:" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови:" + } + } + } }, "Long Name" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langer Name" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дуго име" + } + } + } }, "Long Name: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langer Name: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дуго име: %@" + } + } + } + }, + "Long press to favorite or mute the contact or delete a conversation." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дугим притиском на чвор из листе означите као омиљени или искључите звук контакта или обришите разговор." + } + } + } }, "Longitude" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Längengrad" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дужина" + } + } + } }, "lora" : { "localizations" : { @@ -11293,6 +15602,12 @@ "value" : "LoRa" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRA" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11351,6 +15666,12 @@ "value" : "LoRa Konfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRA подешавања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11365,11 +15686,93 @@ } } }, + "lora.signal.strength.bad" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bad" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лош" + } + } + } + }, + "lora.signal.strength.fair" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fair" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прихватљив" + } + } + } + }, + "lora.signal.strength.good" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Good" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добар" + } + } + } + }, + "lora.signal.strength.none" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "None" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Без" + } + } + } + }, "LOW" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "НИЗАК" + } + } + } }, "Managed Device" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управљани уређај" + } + } + } }, "map" : { "localizations" : { @@ -11415,6 +15818,12 @@ "value" : "Mesh Karta" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мапа меша" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11430,29 +15839,54 @@ } }, "Map Options" : { - - }, - "Map Publish Interval" : { - - }, - "Map Report" : { - - }, - "Map Tile Data" : { - - }, - "Map Type" : { - - }, - "map.centering" : { - "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Centering" + "value" : "Kartenoptionen" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције мапе" + } + } + } + }, + "Map Publish Interval" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал објављивања мапе" + } + } + } + }, + "Map Report" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Извештај мапе" + } + } + } + }, + "Map Tile Data" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подаци плочица мапе" + } + } + } + }, + "map.centering" : { + "extractionState" : "manual", + "localizations" : { "en" : { "stringUnit" : { "state" : "translated", @@ -11489,6 +15923,12 @@ "value" : "Centreringsläge" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Режим центрирања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11504,13 +15944,8 @@ } }, "map.recentering" : { + "extractionState" : "stale", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatic Re-centering" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -11547,6 +15982,12 @@ "value" : "Automatisk Centrering" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Аутоматско поновно центрирање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11564,12 +16005,6 @@ "map.tiles.delete" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Cached Map Tiles" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -11606,6 +16041,12 @@ "value" : "Radera Alla Kartplattor" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обриши све плочице мапе" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11626,7 +16067,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "kartentyp" + "value" : "Kartentyp" } }, "en" : { @@ -11665,6 +16106,12 @@ "value" : "Standardtyp" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подразумевани тип" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11724,6 +16171,12 @@ "value" : "Använd Äldre Mesh Karta" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користите легаси мрежну мапу" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11783,6 +16236,12 @@ "value" : "Spårningsläge för användare" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мод праћења корисника" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11803,7 +16262,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Follow" + "value" : "Folgen" } }, "en" : { @@ -11842,6 +16301,12 @@ "value" : "Följ" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прати" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11862,7 +16327,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Follow with heading" + "value" : "Folgen mit Steuerkurs" } }, "en" : { @@ -11901,6 +16366,12 @@ "value" : "Följ med riktning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прати са правцем" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11921,7 +16392,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "None" + "value" : "Keiner" } }, "en" : { @@ -11960,6 +16431,12 @@ "value" : "Ingen" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ни један" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -11975,14 +16452,21 @@ } }, "Mesh activity update" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажурирање активности мреже" + } + } + } }, "mesh.live.activity" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Mesh Live Activity" + "value" : "Mesh Live Aktivität" } }, "en" : { @@ -12021,6 +16505,12 @@ "value" : "Mesh Live Aktivitet" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активности мреже уживо" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12079,6 +16569,12 @@ "value" : "Mesh-logg" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови мреже" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12096,12 +16592,6 @@ "mesh.log.ambientlighting.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ambient Lighting module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12138,6 +16628,12 @@ "value" : "Konfiguration för omgivningsbelysningsmodulen mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљена конфигурација модула амбијенталног осветљења: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12197,6 +16693,12 @@ "value" : "Bluetooth-konfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљена конфигурација блутута: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12214,12 +16716,6 @@ "mesh.log.cannedmessage.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canned Message module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12256,6 +16752,12 @@ "value" : "Konfiguration för modulen med fördefinierade meddelanden mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула за унапред припремљене поруке примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12273,12 +16775,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", @@ -12315,6 +16811,12 @@ "value" : "Begärda meddelanden för modulen med fördefinierade meddelanden för nod: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтеване поруке модула за унапред припремљене поруке за чвор: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12332,12 +16834,6 @@ "mesh.log.cannedmessages.messages.received %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canned Messages Messages Received For: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12374,6 +16870,12 @@ "value" : "Mottagna meddelanden för fördefinierade meddelanden För: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљене поруке за унапред припремљене поруке за: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12388,74 +16890,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", @@ -12492,6 +16929,12 @@ "value" : "Skickade en kanal för: %@ Kanalindex %d" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Послат је канал за: %@ Индекс канала %d" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12509,12 +16952,6 @@ "mesh.log.detectionsensor.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detection Sensor module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12551,6 +16988,12 @@ "value" : "Konfiguration för detektionssensormodulen mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула за сензор детекције примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12571,7 +17014,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Geräte Konfiguration empfangen: %@" + "value" : "Gerätekonfiguration empfangen: %@" } }, "en" : { @@ -12610,6 +17053,12 @@ "value" : "Enhetskonfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљена конфигурација уређаја: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12630,7 +17079,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Device Metadata received from: %@" + "value" : "Device Metadata empfangen von: %@" } }, "en" : { @@ -12669,6 +17118,12 @@ "value" : "Metadata för enhet mottagen från: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Метаподаци уређаја примљени од: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12728,6 +17183,12 @@ "value" : "Begär metadata för enhet för %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевање метаподатака уређаја за %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12787,6 +17248,12 @@ "value" : "Skärmkonfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљена конфигурација приказа: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12804,12 +17271,6 @@ "mesh.log.externalnotification.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "External Notification module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -12846,6 +17307,12 @@ "value" : "Konfiguration för modulen för externa notifikationer mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула за екстерне нотификације примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12866,7 +17333,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "LoRa config received: %@" + "value" : "LoRa config empfangen: %@" } }, "en" : { @@ -12905,6 +17372,12 @@ "value" : "LoRa-konfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација LoRA примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12925,7 +17398,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a LoRa.Config for: %@" + "value" : "LoRa.Config gesendet für: %@" } }, "en" : { @@ -12964,6 +17437,12 @@ "value" : "Skickade en LoRa.Konfiguration för: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Послата LoRA конфигурација за: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -12984,7 +17463,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "MQTT module config received: %@" + "value" : "MQTT Modulkonfiguration empfangen: %@" } }, "en" : { @@ -13023,6 +17502,12 @@ "value" : "MQTT-modulkonfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација MQTT модула примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13043,7 +17528,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "MyInfo received: %@" + "value" : "MyInfo empfangen: %@" } }, "en" : { @@ -13082,6 +17567,12 @@ "value" : "Min info mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Моје информације примљене: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13102,7 +17593,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Netzwerk onfiguration empfangen: %@" + "value" : "Netzwerkkonfiguration empfangen: %@" } }, "en" : { @@ -13141,6 +17632,12 @@ "value" : "Nätverkskonfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација мреже примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13161,7 +17658,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Node info empfangen für: %@" + "value" : "Knoteninformation empfangen für: %@" } }, "en" : { @@ -13200,6 +17697,12 @@ "value" : "Nodinformation mottagen för: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Информације о чвору примљене за: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13253,6 +17756,12 @@ "value" : "PAX-räknarmeddelande mottaget från: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Порука PAX бројача примљена од: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13287,6 +17796,12 @@ "state" : "translated", "value" : "PAX-räknarkonfiguration mottagen: %@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација PAX бројача примљена: %@" + } } } }, @@ -13296,7 +17811,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Positions Konfiguration empfangen: %@" + "value" : "Positionskonfiguration empfangen: %@" } }, "en" : { @@ -13335,6 +17850,12 @@ "value" : "Positionskonfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација позиције примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13355,7 +17876,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Positionspaket empfangen von Node: %@" + "value" : "Position empfangen von Knoten: %@" } }, "en" : { @@ -13394,6 +17915,12 @@ "value" : "Positionspaket mottaget från nod: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пакет позиције примљен од чвора: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13428,6 +17955,12 @@ "state" : "translated", "value" : "Strömkonfiguration mottagen: %@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација напајања примљена: %@" + } } } }, @@ -13476,6 +18009,12 @@ "value" : "Konfiguration för räckviddstestmodulen mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула теста домета примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13496,7 +18035,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "RTTTL Ringtone config received: %@" + "value" : "RTTTL Klingeltonkonfiguration empfangen: %@" } }, "en" : { @@ -13535,6 +18074,12 @@ "value" : "Konfiguration för RTTTL-ringsignal mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација RTTTL мелодије примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13594,6 +18139,12 @@ "value" : "Routing mottagen för RequestID: %@ Ack Status: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рутирање примљено за ИД захтева: %@ Статус потврде: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13653,6 +18204,12 @@ "value" : "Seriekonfigurationsmodul mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација серијског модула примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13673,7 +18230,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" : { @@ -13712,6 +18269,12 @@ "value" : "Skickade ett positionspaket från Apple-enhetens GPS till nod: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позициони пакет послат са Епл уређаја на чвор: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13729,12 +18292,6 @@ "mesh.log.storeforward.config %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Store & Forward module config received: %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -13771,6 +18328,12 @@ "value" : "Konfiguration för Store & Forward-modulen mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула за чување и прослеђивање примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13830,6 +18393,12 @@ "value" : "Telemetrimodulkonfiguration mottagen: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула телеметрије примљена: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13889,6 +18458,12 @@ "value" : "Telemetri mottagen för: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Телеметрија примљена за: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -13948,6 +18523,12 @@ "value" : "Meddelande mottaget från textmeddelandeappen." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Порука примљена из апликације за текстуалне поруке." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14007,6 +18588,12 @@ "value" : "Misslyckades med att skicka meddelande, inte korrekt ansluten till %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Слање поруке није успело, није правилно повезано са: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14066,6 +18653,12 @@ "value" : "Skickade meddelande %@ från %@ till %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Порука послата %@ са %@ на %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14086,7 +18679,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Traceroute Anforderung an node gesendet: %@ wurde direkt empfangen." + "value" : "Traceroute Anforderung an Knoten gesendet: %@ wurde direkt empfangen." } }, "en" : { @@ -14125,6 +18718,12 @@ "value" : "Spårruttförfrågan skickad till nod: %@ mottogs direkt." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за тражење путања послат на чвор: %@ је примљен директно." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14184,6 +18783,12 @@ "value" : "Spårruttförfrågan returnerade: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за тражење путања враћен: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14204,7 +18809,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sende Traceroute Anforderung zu Mode: %@" + "value" : "Sende Traceroute Anforderung zu Knoten: %@" } }, "en" : { @@ -14243,6 +18848,12 @@ "value" : "Skickade en spårruttförfrågan till nod: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за тражење путања послат на чвор: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14260,12 +18871,6 @@ "mesh.log.wantconfig %@" : { "extractionState" : "migrated", "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Issuing Want Config to %@" - } - }, "en" : { "stringUnit" : { "state" : "translated", @@ -14302,6 +18907,12 @@ "value" : "Utfärdar Want Config till %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Издавање захтева за конфигурацију на: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14322,7 +18933,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Waypoint Packet received from node: %@" + "value" : "Wegpunkt von Knoten empfangen: %@" } }, "en" : { @@ -14361,6 +18972,12 @@ "value" : "Vägpunktspaket mottaget från nod: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пакет са тачкама пута примљен од чвора: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14381,7 +18998,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sent a Waypoint Packet from: %@" + "value" : "Wegpunkt gesendet von: %@" } }, "en" : { @@ -14420,6 +19037,12 @@ "value" : "Skickade en vägpunktspaket från: %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пакет са тачкама пута послат од: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14435,10 +19058,30 @@ } }, "Meshtastic Node %@ has shared channels with you" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic Knoten %@ hat Kanäle mit dir geteilt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic чвор %@ је поделио канале са вама." + } + } + } }, "Meshtastic® Copyright Meshtastic LLC" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic® Ауторска права Meshtastic LLC" + } + } + } }, "message" : { "localizations" : { @@ -14484,6 +19127,12 @@ "value" : "Meddelande" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Порука" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14498,6 +19147,48 @@ } } }, + "Message" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Порука" + } + } + } + }, + "Message content exceeds 200 bytes." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichteninhalt überschreitet 200 Bytes." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Садржај поруке премашује 200 бајтова." + } + } + } + }, + "Message Status Options" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције статуса поруке" + } + } + } + }, "message.details" : { "localizations" : { "de" : { @@ -14542,6 +19233,12 @@ "value" : "Meddelandedetaljer" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Детаљи поруке" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14600,6 +19297,12 @@ "value" : "Meddelanden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поруке" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14615,22 +19318,88 @@ } }, "Messages" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поруке" + } + } + } }, "Messages separate with |" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten getrennt mit |" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поруке се раздвајају са |" + } + } + } }, "Minimum Distance" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum Distanz" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минимум раздаљине" + } + } + } }, "Minimum Interval" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum Intervall" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минимални интервал" + } + } + } }, "Minimum time between detection broadcasts" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минимално време између емитовања детекције" + } + } + } }, "Mininum time between detection broadcasts. Default is 45 seconds." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минимално време између емитовања детекције. Подразумевано је 45 секунди." + } + } + } }, "mode" : { "localizations" : { @@ -14676,6 +19445,12 @@ "value" : "Läge" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мод" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14690,6 +19465,16 @@ } } }, + "Model" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Модел" + } + } + } + }, "module.configuration" : { "localizations" : { "de" : { @@ -14734,6 +19519,12 @@ "value" : "Modulkonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација модула" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14792,6 +19583,12 @@ "value" : "MQTT" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14807,7 +19604,14 @@ } }, "MQTT" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT" + } + } + } }, "mqtt.clientproxy" : { "localizations" : { @@ -14853,6 +19657,12 @@ "value" : "MQTT-klientproxy" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT посредник клијента" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14872,7 +19682,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "MQTT Config" + "value" : "MQTT Konfiguration" } }, "en" : { @@ -14911,6 +19721,12 @@ "value" : "MQTT-konfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT подешавања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14931,7 +19747,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Connect to MQTT" + "value" : "Verbunden mit MQTT" } }, "en" : { @@ -14970,6 +19786,12 @@ "value" : "Anslut till MQTT" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повежи се на MQTT" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14990,7 +19812,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Disconnect from MQTT" + "value" : "Trennen von MQTT" } }, "en" : { @@ -15029,6 +19851,12 @@ "value" : "Koppla från MQTT" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Развежи се од MQTT" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15087,6 +19915,12 @@ "value" : "Användarnamn" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Корисничко име" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15101,8 +19935,25 @@ } } }, + "Must be a single emoji" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мора бити један емотикон" + } + } + } + }, "Nag timeout" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Период чекања је истекао" + } + } + } }, "name" : { "localizations" : { @@ -15148,6 +19999,12 @@ "value" : "Namn" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Име" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15163,10 +20020,46 @@ } }, "Name" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Име" + } + } + } + }, + "Name must be less than 30 bytes" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Name muss kürzer als 30 Bytes sein" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Име мора бити краће од 30 бајтова" + } + } + } }, "Nearby Topics" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Теме у окружењу" + } + } + } }, "network" : { "localizations" : { @@ -15212,6 +20105,12 @@ "value" : "Nätverk" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мрежа" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15227,10 +20126,24 @@ } }, "Network Status Orange" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Статус мреже: Наранџаст" + } + } + } }, "Network Status Red" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Статус мреже: Црвен" + } + } + } }, "network.config" : { "localizations" : { @@ -15276,6 +20189,12 @@ "value" : "Nätverkskonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација мреже" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15291,22 +20210,110 @@ } }, "Never" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Никада" + } + } + } + }, + "New Node" : { + "extractionState" : "manual", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нови чвор" + } + } + } + }, + "New Node has been discovered" : { + "extractionState" : "manual", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откривен је нови чвор" + } + } + } }, "Newer firmware is available" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neuere Firmware ist verfügbar" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нова верзија фирмвера је доступна" + } + } + } + }, + "No Connected Node" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kein verbundener Knoten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема повезаног чвора" + } + } + } }, "No Device Metrics" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема метрика уређаја." + } + } + } }, "No Environment Metrics" : { - - }, - "No Logs Available" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема метрика окружења" + } + } + } }, "No Positions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Positionen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема позиција" + } + } + } }, "no.nodes" : { "extractionState" : "manual", @@ -15314,7 +20321,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Meshtastic Nodes gefunden" + "value" : "Keine Meshtastic Knoten gefunden" } }, "en" : { @@ -15353,6 +20360,12 @@ "value" : "Inga Meshtastic-noder hittades" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема пронађених Мештастик чворова" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15368,7 +20381,20 @@ } }, "Node" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чвор" + } + } + } }, "Node Core Data Backup %@/%@ - %@ - %@" : { "localizations" : { @@ -15377,20 +20403,88 @@ "state" : "new", "value" : "Node Core Data Backup %1$@/%2$@ - %3$@ - %4$@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Резервна копија података језгра чвора %1$@/%2$@ - %3$@ - %4$@" + } + } + } + }, + "Node does not have positions" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten hat keine Position" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чвор нема позиције" + } } } }, "Node History" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten Historie" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Историја чвора" + } + } + } }, "Node Info Broadcast Interval" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал емитовања информација о чвору" + } + } + } }, "Node Map" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotenkarte" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мапа чворова" + } + } + } }, "Node Number" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotennummer" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број чвора" + } + } + } }, "nodelist.filter.distance %@" : { "extractionState" : "migrated", @@ -15398,7 +20492,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "up to %@ away" + "value" : "bis zu %@ entfernt" } }, "en" : { @@ -15437,6 +20531,12 @@ "value" : "upp till %@ bort" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "удаљено до максималних %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15456,7 +20556,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nodes" + "value" : "Knoten" } }, "en" : { @@ -15489,6 +20589,12 @@ "value" : "Noder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чворови" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15509,7 +20615,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nodes (%@)" + "value" : "Knoten (%@)" } }, "en" : { @@ -15548,6 +20654,12 @@ "value" : "Noder (%@)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чворови (%@)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15563,7 +20675,14 @@ } }, "Not a valid route file" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није валидна датотека путања" + } + } + } }, "not.connected" : { "localizations" : { @@ -15609,6 +20728,12 @@ "value" : "Ingen enhet ansluten" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема повезаних уређаја" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15624,19 +20749,84 @@ } }, "Notes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Белешке" + } + } + } }, "Num: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број: %@" + } + } + } }, "Number of hops" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl Hops" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број хопова" + } + } + } }, "Number of records" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl Einträge" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број записа" + } + } + } }, "Number of satellites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl Satelliten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број сателита" + } + } + } }, "numbers.punctuation" : { "extractionState" : "manual", @@ -15683,6 +20873,12 @@ "value" : "Siffror och skiljetecken" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бројеви и интерпункција" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15742,6 +20938,12 @@ "value" : "Av" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искључен" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15801,6 +21003,12 @@ "value" : "Offline" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ван мреже" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15814,15 +21022,48 @@ } } } - }, - "Offline Maps" : { - }, "OK" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ОК" + } + } + } + }, + "Ok to MQTT" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позитиван за MQTT" + } + } + } }, "OLED Type" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "OLED Typ" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тип OLED-а" + } + } + } }, "on.boot" : { "extractionState" : "migrated", @@ -15869,6 +21110,12 @@ "value" : "Endast vid uppstart" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Само при покретању" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15884,25 +21131,115 @@ } }, "Onboarding for licensed operators requires firmware 2.0.20 or greater. Make sure to refer to your local regulations and contact the local amateur frequency coordinators with questions." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увођење за лиценциране оператере захтева фирмвер верзије 2.0.20 или новије. Уверите се да се придржавате локалних прописа и обратите се локалним координаторима за аматерске фреквенције са питањима." + } + } + } }, "One Hour" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine Stunde" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Један сат" + } + } + } }, "One Minute" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine Minute" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Једна минута" + } + } + } }, "Online" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Online" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "На мрежи" + } + } + } }, "Open Settings" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen öffnen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отвори подешавања" + } + } + } + }, + "optimized.for.2.color.displays" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optimized for 2 color displays" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оптимизовано за двобојне дисплеје" + } + } + } }, "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" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опциони поља за укључивање при склапању порука о позицији. Што више поља је укључено, порука ће бити већа, што доводи до дужег времена емитовања и већег ризика од губитка пакета" + } + } + } }, "Optional GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опциони GPIO" + } + } + } }, "options" : { "localizations" : { @@ -15948,6 +21285,12 @@ "value" : "Alternativ" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15963,31 +21306,110 @@ } }, "Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optionen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције" + } + } + } }, "OS Log Entry Details" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Детаљи уноса ОС дневника" + } + } + } }, "OTA Updates are not supported on the this NRF Device." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ОТА ажурирања нису подржана на овом NRF уређају." + } + } + } }, "OTA Updates are not supported on your platform." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ОТА ажурирања нису подржана на вашој платформи." + } + } + } }, "Other data sources" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Остали извори података" + } + } + } + }, + "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Излаз дебаговања уживо преко серијског интерфејса, прегледајте и извозите логове уређаја са редукованим позицијама преко блутута." + } + } + } }, "Output pin buzzer GPIO " : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Излазни пин за зујалицу GPIO" + } + } + } }, "Output pin GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Излазни пин GPIO" + } + } + } }, "Output pin vibra GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Излазни пин за вибрацију GPIO" + } + } + } }, "Override automatic OLED screen detection." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Премаши аутоматско откривање OLED екрана." + } + } + } }, "password" : { "localizations" : { @@ -16033,6 +21455,12 @@ "value" : "Lösenord" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лозинка" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16091,6 +21519,12 @@ "value" : "Pausa" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Паузирај" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16124,6 +21558,12 @@ "state" : "translated", "value" : "BLE" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "БЛЕ" + } } } }, @@ -16146,6 +21586,12 @@ "state" : "translated", "value" : "Inga loggar för PAX-räknare" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема логова PAX бројача" + } } } }, @@ -16168,6 +21614,12 @@ "state" : "translated", "value" : "Radera all paxdata?" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избриши све PAX податке?" + } } } }, @@ -16190,6 +21642,12 @@ "state" : "translated", "value" : "PAX-räknarens logg" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови PAX бројача" + } } } }, @@ -16212,6 +21670,12 @@ "state" : "translated", "value" : "Totalt PAX" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укупно PAX" + } } } }, @@ -16234,6 +21698,28 @@ "state" : "translated", "value" : "WiFi" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ВајФај" + } + } + } + }, + "Perform a factory reset on the node you are connected to" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbundenen Knoten auf Werkseinstellungen zurücksetzen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изврши фабричко ресетовање чвора на који сте повезани" + } } } }, @@ -16281,6 +21767,12 @@ "value" : "Telefon-GPS" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS телефона" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16340,6 +21832,12 @@ "value" : "Hur ofta din telefon skickar din plats till enheten, platsuppdateringar till mesh-nätverket hanteras av enheten." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често ваш телефон шаље вашу локацију уређају, ажурирања локације на мрежу се управљају од стране уређаја." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16355,19 +21853,70 @@ } }, "Pin %lld" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пин %lld" + } + } + } }, "Pin A" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пин А" + } + } + } }, "Pin B" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пин Б" + } + } + } + }, + "PKI based node administration, requires firmware version 2.5+" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKI-basierte Knotenadministration, benötigt Firmware Version 2.5+" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Администрација чвора заснована на PKI захтева фирмвер верзију 2.5 или новију" + } + } + } }, "Please connect to a radio to configure settings." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Молимо вас да се повежете на радио да бисте конфигурисали подешавања." + } + } + } }, "Points of Interest" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тачке интересовања" + } + } + } }, "position" : { "localizations" : { @@ -16413,6 +21962,12 @@ "value" : "Position" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позиција" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16427,20 +21982,81 @@ } } }, + "Position Exchange Failed" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспела размена позиција" + } + } + } + }, + "Position Exchange Requested" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевана размена позиција" + } + } + } + }, "Position Flags" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Заставице позиције" + } + } + } }, "Position Log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Логови позиција" + } + } + } }, "Position Log %lld Points" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дневник позиција %lld тачака" + } + } + } }, "Position Packet" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пакети позиција" + } + } + } }, "Position Sent" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Position gesendet" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позиција послата" + } + } + } }, "position.config" : { "localizations" : { @@ -16486,6 +22102,12 @@ "value" : "Positionskonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања позиције" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16503,6 +22125,12 @@ "position.precision %@" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Innerhalb %@" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -16520,32 +22148,106 @@ "state" : "translated", "value" : "Inom %@" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "У кругу %@" + } } } }, "Positions Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позиционирање укључено" + } + } + } }, "Positions will be provided by your device GPS, if you select disabled or not present you can set a fixed position." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Позиције ће бити обезбеђене путем GPS-а вашег уређаја. Ако одаберете опцију „онемогућено“ или „није присутно“, можете подесити фиксну позицију." + } + } + } }, "Power Metrics" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мерни подаци о снази" + } + } + } }, "Power Off" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искључи" + } + } + } }, "Power Options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције снаге" + } + } + } }, "Power Screen" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Снага екрана" + } + } + } }, "Powered" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Angeschaltet" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Напајано" + } + } + } }, "Precise Location" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genaue Position" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прецизне локације" + } + } + } }, "preferred.radio" : { "extractionState" : "manual", @@ -16592,6 +22294,12 @@ "value" : "Föredragen Radio" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преферирани радио" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16607,28 +22315,141 @@ } }, "Presets" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Унапред подешено" + } + } + } }, "Press Pin" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Притисни пин" + } + } + } }, "PRESSURE" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DRUCK" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ПРИТИСАК" + } + } + } }, "Primary" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Основни" + } + } + } + }, + "Primary Admin Key" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Основни административни кључ" + } + } + } }, "Primary GPIO" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Основни GPIO" + } + } + } + }, + "Private Key" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приватни кључ" + } + } + } }, "Project information" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Информације о пројекту" + } + } + } + }, + "Public Key" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јавни кључ" + } + } + } + }, + "Public Key Encryption" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шифровање јавним кљулем" + } + } + } + }, + "Public Key Mismatch" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неслагање јавних кључева" + } + } + } }, "PWD" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PWD" + } + } + } }, - "Radar" : { - + "Radio Disconnected" : { + "extractionState" : "manual", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Радио веза је прекинута" + } + } + } }, "radio.configuration" : { "localizations" : { @@ -16674,6 +22495,12 @@ "value" : "Radioinställningar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација радио уређаја" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16732,6 +22559,12 @@ "value" : "Räckviddstest" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тест домета" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16791,6 +22624,12 @@ "value" : "Blockera räckviddstest" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тест домета блока" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16849,6 +22688,12 @@ "value" : "Konfiguration av räckviddstest" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација теста домета" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16868,7 +22713,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Reboot" + "value" : "Neustart" } }, "en" : { @@ -16907,6 +22752,12 @@ "value" : "Starta om" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поновно покретање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16921,12 +22772,28 @@ } } }, + "Reboot Node?" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten neustarten?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поново покрени чвор?" + } + } + } + }, "reboot.node" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Node neustarten?" + "value" : "Knoten neustarten?" } }, "en" : { @@ -16965,6 +22832,12 @@ "value" : "Starta om nod?" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поново покрени чвор?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -16980,10 +22853,24 @@ } }, "Rebroadcast Mode" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Режим реемитовања" + } + } + } }, "Receive data (rxd) GPIO pin" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пријемни податак (rxd) GPIO пин" + } + } + } }, "received.ack" : { "localizations" : { @@ -17029,6 +22916,12 @@ "value" : "Mottaget kvitto" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљен ACK" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17087,6 +22980,12 @@ "value" : "Mottagarkvitto" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прималац ACK" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17102,17 +23001,56 @@ } }, "Recording route" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Route aufzeichnen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Снимање руте" + } + } + } }, "Refresh device metadata" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Освежи метаподатке уређаја" + } + } + } }, "Region" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Region" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Регион" + } + } + } }, "relativetimeofday.afternoon" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachmittag" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17124,12 +23062,24 @@ "state" : "translated", "value" : "Tarde" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пре подне" + } } } }, "relativetimeofday.evening" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abend" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17141,12 +23091,24 @@ "state" : "translated", "value" : "Noite" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вече" + } } } }, "relativetimeofday.midday" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mittag" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17158,12 +23120,24 @@ "state" : "translated", "value" : "Meio-dia" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подне" + } } } }, "relativetimeofday.morning" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Morgen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17175,12 +23149,24 @@ "state" : "translated", "value" : "Manhã" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јутро" + } } } }, "relativetimeofday.nighttime" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nacht" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17192,26 +23178,106 @@ "state" : "translated", "value" : "Noite" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ноћ" + } } } }, "Release Notes" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Белешке о издању" + } + } + } }, "Remote administration for: %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Даљинска администрација за: %@" + } + } + } }, - "Remote: %@" : { - + "Remote Legacy Admin: %@" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Администрација застарелих система на даљину: %@" + } + } + } + }, + "Remote PKI Admin: %@" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Администрација PKI на даљину: %@" + } + } + } }, "Remove" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entfernen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уклони" + } + } + } }, "Remove from favorites" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Von Favoriten entfernen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уклони из омиљених" + } + } + } + }, + "Remove from ignored" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уклони из игнорисаних" + } + } + } }, "Replace Channels" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Замени канале" + } + } + } }, "reply" : { "localizations" : { @@ -17257,6 +23323,12 @@ "value" : "Svara" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одговори" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17271,24 +23343,122 @@ } } }, - "Request Admin: %@" : { - + "Request Legacy Admin: %@" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевај администрацију застарелих система: %@" + } + } + } + }, + "Request PKI Admin: %@" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтевај PKI администрацију: %@" + } + } + } }, "Requires that there be an accelerometer on your device." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтева да уређај има акцелерометар." + } + } + } + }, + "Reset App Settings" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Einstellungen zurücksetzen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ресетовање подешавања апликације" + } + } + } }, "Reset NodeDB" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knotendatenbank zurücksetzen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ресетовање базе чворова (NodeDB)" + } + } + } + }, + "Restart" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neustarten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поновно покретање" + } + } + } + }, + "Restart to the node you are connected to" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbundenen Knoten neustarten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поновно покретање на чвор на који сте повезани" + } + } + } }, "restore" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiederherstellen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обнова" + } + } + } }, "resume" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Resume" + "value" : "Fortsetzen" } }, "en" : { @@ -17327,6 +23497,12 @@ "value" : "Återuppta" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настави" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17342,14 +23518,27 @@ } }, "Review the app" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App bewerten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцените апликацију" + } + } + } }, "ringtone" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ringtone" + "value" : "Klingelton" } }, "en" : { @@ -17388,6 +23577,12 @@ "value" : "Ringsignal" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мелодија звона" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17408,7 +23603,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ringtone Config" + "value" : "Klingelton Konfiguration" } }, "en" : { @@ -17447,6 +23642,12 @@ "value" : "Ringsignalsinställningar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавање мелодије звона" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17462,28 +23663,118 @@ } }, "Role" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rolle" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улога" + } + } + } }, "Role: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rolle: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улога: %@" + } + } + } }, "Roles" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rollen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Улоге" + } + } + } }, "Root Topic" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Корен тема" + } + } + } }, "Rotary 1" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ротациони 1" + } + } + } + }, + "Route Back: %@" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Путања назад: %@" + } + } + } }, "Route Lines" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Линије руте" + } + } + } }, "Route recording paused" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Снимање руте паузирано" + } + } + } }, "Route: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Route: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рута: %@" + } + } + } }, "route.recorder" : { "localizations" : { @@ -17529,6 +23820,12 @@ "value" : "Ruttinspelare" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Снимач руте" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17544,17 +23841,31 @@ } }, "Router" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рутер" + } + } + } }, "Router Options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције рутера" + } + } + } }, "routes" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Routes" + "value" : "Routen" } }, "en" : { @@ -17593,6 +23904,12 @@ "value" : "Rutter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Руте" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17610,6 +23927,12 @@ "routes.activitytype.biking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biken" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17621,12 +23944,24 @@ "state" : "translated", "value" : "Passeio de Bicicleta" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вожња бицикле" + } } } }, "routes.activitytype.driving" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17638,12 +23973,24 @@ "state" : "translated", "value" : "Conduzir" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вожња аута" + } } } }, "routes.activitytype.filename.biking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "biken" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17655,12 +24002,24 @@ "state" : "translated", "value" : "Passeio de Bicicleta" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "тура бициклом" + } } } }, "routes.activitytype.filename.driving" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "fahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17672,12 +24031,24 @@ "state" : "translated", "value" : "Conduzir" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "вожња" + } } } }, "routes.activitytype.filename.hiking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "wandern" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17689,6 +24060,12 @@ "state" : "translated", "value" : "Caminhar na Montanha" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "планинарње" + } } } }, @@ -17706,12 +24083,24 @@ "state" : "translated", "value" : "Caminhar overland" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вожња преко копна" + } } } }, "routes.activitytype.filename.skiing" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "skitour" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17723,12 +24112,24 @@ "state" : "translated", "value" : "Passeio de esqui" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ски тура" + } } } }, "routes.activitytype.filename.walking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "gehen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17740,12 +24141,24 @@ "state" : "translated", "value" : "Caminhar" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "шетња" + } } } }, "routes.activitytype.hiking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wandern" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17757,6 +24170,12 @@ "state" : "translated", "value" : "Caminhada na Montanha" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Планинарење" + } } } }, @@ -17774,12 +24193,24 @@ "state" : "translated", "value" : "Overlanding" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оверлендинг" + } } } }, "routes.activitytype.skiing" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skifahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17791,12 +24222,24 @@ "state" : "translated", "value" : "Esqui" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скијање" + } } } }, "routes.activitytype.walking" : { "extractionState" : "migrated", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gehen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17808,6 +24251,12 @@ "state" : "translated", "value" : "Caminhada" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шетња" + } } } }, @@ -17856,6 +24305,12 @@ "value" : "Bekräftad" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потврђено" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17915,6 +24370,12 @@ "value" : "Felaktig begäran" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лош захтев" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -17974,6 +24435,12 @@ "value" : "Regionala sändningsgränsen nådd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Достигнут регионални лимит радног циклуса" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18033,6 +24500,12 @@ "value" : "Mottog ett negativt kvitto" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примљено негативно признање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18092,6 +24565,12 @@ "value" : "Max antal omsändningar nått" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Достигнут максималан број поновних слања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18151,6 +24630,12 @@ "value" : "Ingen kanal" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема канала" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18171,7 +24656,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kein Interface" + "value" : "Keine Schnittstelle" } }, "en" : { @@ -18210,6 +24695,12 @@ "value" : "Inget gränssnitt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема интерфејса" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18269,6 +24760,12 @@ "value" : "Inget svar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема одговора" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18328,6 +24825,12 @@ "value" : "Ingen rutt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нема руте" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18387,6 +24890,12 @@ "value" : "Inte auktoriserad" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није ауторизовано" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18401,6 +24910,52 @@ } } }, + "routing.pkifailed" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschlüsseltes Senden fehlgeschlagen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encrypted Send Failed" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шифровано слање није успело" + } + } + } + }, + "routing.pkiunknownpubkey" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbekannter öffentlicher Schlüssel" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown Public Key" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Непознат јавни кључ" + } + } + } + }, "routing.timeout" : { "extractionState" : "migrated", "localizations" : { @@ -18446,6 +25001,12 @@ "value" : "Tidsgräns överskriden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време истекло" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18505,6 +25066,12 @@ "value" : "Paketet är för stort" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пакет је превелики" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18520,16 +25087,44 @@ } }, "RSSI %@ dBm" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %@ dBm" + } + } + } }, "RSSI %ddB" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %ddB" + } + } + } }, "RSSI %llddB" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %llddB" + } + } + } }, "RX Boosted Gain" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Појачање пријемника" + } + } + } }, "satellite" : { "extractionState" : "migrated", @@ -18576,6 +25171,12 @@ "value" : "Satellit" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сателит" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18635,6 +25236,12 @@ "value" : "Satellitöverflygning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прелет сателита" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18650,13 +25257,52 @@ } }, "Sats" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satelliten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сателита" + } + } + } }, "Sats Estimate %lld" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satelliten Schätzung %lld" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Процена броја сателита %lld" + } + } + } }, "Sats in view: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satelliten in Sicht: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сателити на видику: %@" + } + } + } }, "save" : { "localizations" : { @@ -18702,6 +25348,12 @@ "value" : "Spara" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сачувај" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18717,10 +25369,36 @@ } }, "Save" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Speichern" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сачувај" + } + } + } }, "Save User Config to %@?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benutzerkonfiguration nach %@ speichern?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сачувати корисничу конфигурацију за %@?" + } + } + } }, "save.config %@" : { "extractionState" : "migrated", @@ -18728,7 +25406,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Save Config for %@" + "value" : "Speichere Konfiguration für %@" } }, "en" : { @@ -18767,6 +25445,12 @@ "value" : "Spara konfiguration för %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сачувати конфигурацију за %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18782,31 +25466,158 @@ } }, "Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Снима CSV са детаљима порука теста домета, тренутно доступно само на ESP32 уређајима са веб сервером." + } + } + } }, "Screen on for" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екран укључен за" + } + } + } }, "Search" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suchen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Претражи" + } + } + } + }, + "Second" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Други" + } + } + } }, "Secondary" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Секундарни" + } + } + } + }, + "Secondary Admin Key" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Секундарни административни кључ" + } + } + } + }, + "Security" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sicherheit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сигурност" + } + } + } + }, + "Security Config" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sicherheitskonfiguration" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сигурносна подешавања" + } + } + } + }, + "Security Config Settings require a firmware version 2.5+" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сигурносна подешавања захтевају фирмвер верзију 2.5+" + } + } + } }, "Select a channel" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kanal wählen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одабери канал" + } + } + } }, "Select a conversation" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изабери разговор" + } + } + } }, "Select a conversation type" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изабери тип разговора" + } + } + } }, "Select a Trace Route" : { - - }, - "Select something to view" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изабери пут праћења кроз мрежу" + } + } + } }, "select.contact" : { "extractionState" : "manual", @@ -18853,6 +25664,12 @@ "value" : "Välj en kontakt" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одабери контакт" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18912,6 +25729,12 @@ "value" : "Välj ett alternativ från menyn" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изабери ставку из менија" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18931,7 +25754,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Node auswählen" + "value" : "Knoten auswählen" } }, "en" : { @@ -18970,6 +25793,12 @@ "value" : "Välj en nod" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одабери чвор" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -18985,34 +25814,222 @@ } }, "Send" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи" + } + } + } + }, + "Send ${messageContent} to ${channelNumber}" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sende ${messageContent} an ${channelNumber}" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи ${messageContent} на ${channelNumber}" + } + } + } + }, + "Send a Group Message" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppennachricht senden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи групну поруку" + } + } + } + }, + "Send a message to a certain meshtastic channel" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи поруку на одређени месхтастичан канал" + } + } + } + }, + "Send a position on the primary channel when the user button is triple clicked." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи позицију на примарном каналу када се корисничко дугме три пута кликне." + } + } + } + }, + "Send a shutdown to the node you are connected to" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herunterfahren an verbundenen Knoten senden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи искључење чвору на који си повезан" + } + } + } + }, + "Send a Waypoint" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wegpunkt senden" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи тачку путање" + } + } + } }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи ASCII звона са поруком упозорења. Корисно за покретање спољашњег обавештења на звону." + } + } + } }, "Send Bell" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sende Glocke" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи звоно" + } + } + } }, "Send Reboot OTA" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи сигнал поновног покретања (OTA)" + } + } + } }, "Sender Interval" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Инерварл пошиљаоца" + } + } + } }, "Sensor Metrics" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Метрике сензора" + } + } + } }, "Sensor options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције сензора" + } + } + } }, "Sensor Options" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције сензора" + } + } + } + }, + "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Послато другим чворовима на меш мрежи како би им омогућило да израчунају заједнички тајни кључ." + } + } + } }, "Sequence number" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sequenznummer" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Број секвенце" + } + } + } }, "Sequence: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sequenz: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Секвенца: %@" + } + } + } }, "serial" : { "localizations" : { @@ -19058,6 +26075,12 @@ "value" : "Serie" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Серијска веза" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19073,7 +26096,24 @@ } }, "Serial Console" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Серијска конзола" + } + } + } + }, + "Serial Console over the Stream API." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Серијска конзола преко Stream API-ја." + } + } + } }, "serial.config" : { "localizations" : { @@ -19119,6 +26159,12 @@ "value" : "Seriekonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања серијске везе" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19178,6 +26224,12 @@ "value" : "Standard" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Основни" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19237,6 +26289,12 @@ "value" : "NMEA-positioner" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "NMEA позиције" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19296,6 +26354,12 @@ "value" : "Protobufs" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Протобафови" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19355,6 +26419,12 @@ "value" : "Enkel" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Једноставни" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19375,7 +26445,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Text Nachricht" + "value" : "Textnachricht" } }, "en" : { @@ -19414,6 +26484,12 @@ "value" : "Textmeddelande" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текстуална порука" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19429,16 +26505,56 @@ } }, "Server" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Server" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сервер" + } + } + } }, "Server Address" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serveradresse" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Адреса сервера" + } + } + } }, "Set" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подеси" + } + } + } }, "Set the GPIO pins for RXD and TXD." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подеси GPIO пинове за RXD и TXD." + } + } + } }, "set.region" : { "localizations" : { @@ -19484,6 +26600,12 @@ "value" : "Ställ in LoRa-region" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подеси LoRA регион" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19499,7 +26621,14 @@ } }, "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешава максималан број скокова. Подразумевано је 3, а повећање броја одобрених скокова такође повећава загушење и треба га користити опрезно. Поруке емитоване са 0 скокова неће добити потврде пријема (ACK)." + } + } + } }, "settings" : { "localizations" : { @@ -19545,6 +26674,12 @@ "value" : "Inställningar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подешавања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19560,7 +26695,20 @@ } }, "Share QR Code & Link" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR Code & Link teilen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дели QR код и линк" + } + } + } }, "share.channels" : { "localizations" : { @@ -19606,6 +26754,12 @@ "value" : "Dela QR-kod" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дели QR код" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19665,6 +26819,12 @@ "value" : "Dela position" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подели позицију" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19679,71 +26839,315 @@ } } }, + "Shared Key" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemeinsamer Schlüssel" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дељени кључ" + } + } + } + }, "Short Name" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurzname" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кратко име" + } + } + } }, "Short Name: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurzname: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кратко име: %@" + } + } + } }, "Show alerts" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Alarme" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи узбуне" + } + } + } }, "Show Alerts" : { - - }, - "Show Node History" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Alarme" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи узбуне" + } + } + } }, "Show nodes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Knoten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи чворове" + } + } + } }, "Show on device screen" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige auf dem Gerätebildschirm" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи на екрану уређаја" + } + } + } }, "Show on the mesh map." : { - - }, - "Show Route Lines" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige auf der Netzwerkkarte." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи на мапи меш мреже." + } + } + } }, "Show Waypoints " : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige Wegpunkte" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи тачке путање" + } + } + } }, - "Show Weather" : { - + "Shut Down" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herunterfahren" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искључи" + } + } + } + }, + "Shut Down Node?" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten herunterfahren?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искључити чвор?" + } + } + } }, "Shutdown Node?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoten herunterfahren?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искључити чвор?" + } + } + } }, "Signal %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сигнал %@" + } + } + } }, "Smart Position" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Паметно позиционирање" + } + } + } }, "SNR" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR" + } + } + } }, "SNR %@ dB" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@ dB" + } + } + } }, "SNR %@dB" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@dB" + } + } + } }, "Specifies how long the monitored GPIO should output." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одређује колико дуго треба да траје излазни сигнал надзираног GPIO-а." + } + } + } }, "Speed" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geschwindigkeit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Брзина" + } + } + } }, "Speed %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geschwindigkeit %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Брзина %@" + } + } + } }, "Speed: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geschwindigkeit: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Брзина: %@" + } + } + } }, "Spread Factor" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фактор ширења" + } + } + } }, "ssid" : { "localizations" : { @@ -19789,6 +27193,12 @@ "value" : "SSID" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSID" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19848,6 +27258,12 @@ "value" : "Standard" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стандардно" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19907,6 +27323,12 @@ "value" : "Standard Muted" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стандардно мутирано" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19965,6 +27387,12 @@ "value" : "Start" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Почетак" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -19980,13 +27408,34 @@ } }, "State Broadcast Interval" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал емитовања стања" + } + } + } }, "Store and forward clients can request history from routers on the network." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Клијенти за складиштење и прослеђивање могу затражити историју од рутера на мрежи." + } + } + } }, - "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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рутер за складиштење и прослеђивање захтева ESP32 уређај са PSRAM." + } + } + } }, "storeforward" : { "localizations" : { @@ -20032,6 +27481,12 @@ "value" : "Lagra & Videresänd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Складиштење и прослеђивање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20090,6 +27545,12 @@ "value" : "Konfiguration för Lagra & Videresänd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација за складиштење и прослеђивање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20109,7 +27570,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Send Heartbeat" + "value" : "Herzschlag senden" } }, "en" : { @@ -20148,6 +27609,12 @@ "value" : "Skicka hjärtslag" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи откуцај срца" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20167,7 +27634,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Subscribed to mesh" + "value" : "Verbunden mit dem Mesh" } }, "en" : { @@ -20206,6 +27673,12 @@ "value" : "Prenumererar på mesh" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезано са меш мрежом" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20221,17 +27694,37 @@ } }, "Supported" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterstützt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подржан" + } + } + } }, "Supported I2C Connected sensors will be detected automatically, sensors are BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 and SHTC3." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подржани I2C повезани сензори ће бити аутоматски детектовани. Сензори су: BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 и SHTC3." + } + } + } }, "tapback" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tapback Response" + "value" : "Tapback Antwort" } }, "en" : { @@ -20270,6 +27763,12 @@ "value" : "Svarsreaktion" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Реакција додиром" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20329,6 +27828,12 @@ "value" : "Utropstecken" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Узвичник" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20388,6 +27893,12 @@ "value" : "HaHa" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хахаха" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20408,7 +27919,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Gehört" + "value" : "Herz" } }, "en" : { @@ -20447,6 +27958,12 @@ "value" : "Hjärta" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Срце" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20506,6 +28023,12 @@ "value" : "Bajs" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кака" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20565,6 +28088,12 @@ "value" : "Frågetecken" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Знак питања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20624,6 +28153,12 @@ "value" : "Tummen ner" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Палац доле" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20683,6 +28218,12 @@ "value" : "Tummen upp" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лајк" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20703,7 +28244,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wave" + "value" : "Welle" } }, "en" : { @@ -20742,6 +28283,12 @@ "value" : "Vinka" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Махање" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20756,6 +28303,57 @@ } } }, + "telementry.hazardous" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazardous" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опасно" + } + } + } + }, + "telementry.unhealthy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unhealthy" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нездраво" + } + } + } + }, + "telementry.veryUnhealthy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Very Unhealthy" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Веома нездраво" + } + } + } + }, "telemetry" : { "localizations" : { "de" : { @@ -20800,6 +28398,12 @@ "value" : "Telemetri (Sensorer)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Телеметрија (сензори)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20858,6 +28462,12 @@ "value" : "Telemetriinställningar" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација телеметрије" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -20872,101 +28482,504 @@ } } }, + "telemetry.good" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Good" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добро" + } + } + } + }, + "telemetry.moderate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moderate" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Умерено" + } + } + } + }, + "telemetry.sensitive" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unhealthy for Sensitive Groups" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нездраво за осетљиве групе" + } + } + } + }, "Temp" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temp" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Темп." + } + } + } }, "Temperature" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temperatur" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Температура" + } + } + } }, "Ten Minutes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zehn Minuten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Десет пинута" + } + } + } + }, + "Tertiary Admin Key" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Терцијарни административни кључ" + } + } + } + }, + "tft.full.color.displays" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TFT Full Color Displays" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TFT екрани у пуној боји" + } + } + } }, "The amount of time to wait before we consider your packet as done." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време чекања пре него што сматрамо да је ваш пакет завршен." + } + } + } }, "The compass heading on the screen outside of the circle will always point north." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Смер компаса на екрану изван круга увек ће указивати на север." + } + } + } }, "The dew point is %@ right now." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Taupunkt ist gerade %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тачка росе тренутно износи %@." + } + } + } }, "The fastest that position updates will be sent if the minimum distance has been satisfied" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Најбржа брзина којом ће се ажурирати позиција уколико је задовољен минимални услов за растојање." + } + } + } }, "The format used to display GPS coordinates on the device screen." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Формат који се користи за приказивање GPS координата на екрану уређаја." + } + } + } }, "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." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Последња 4 знака MAC адресе уређаја ће бити додата кратком имену како би се подесило BLE име уређаја. Кратко име може бити до 4 бајта дуго." + } + } + } }, "The maximum interval that can elapse without a node broadcasting a position" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимални интервал који може протећи без да чвор емитује позицију." + } + } + } }, "The Meshtastic Apple apps support firmware version %@ and above." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мештастик апликације за Епл уређаје подржавају верзију фирмвера %@ и новије." + } + } + } }, "The minimum distance change in meters to be considered for a smart position broadcast." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Минимална промена растојања у метрима која ће се узети у обзир за паметно емитовање позиције." + } + } + } + }, + "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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Најновији јавни кључ за овај чвор се не подудара са претходно снимљеним кључем. Можете избрисати чвор и дозволити му да поново размени кључеве, али ово такође може указивати на озбиљнији безбедносни проблем. Контактирајте корисника преко другог поузданог канала како бисте утврдили да ли је промена кључа резултат фабричког ресетовања или друге намерне акције." + } + } + } + }, + "The primary public key authorized to send admin messages to this node." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Примарни јавни кључ овлашћен за слање административних порука овом чвору." + } + } + } + }, + "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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јавни кључ се не подудара са снимљеним кључем. Можете избрисати чвор и дозволити му да поново размени кључеве, али ово може указивати на озбиљнији безбедносни проблем. Контактирајте корисника преко другог поузданог канала како бисте утврдили да ли је промена кључа резултат фабричког ресетовања или друге намерне акције." + } + } + } }, "The region where you will be using your radios." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Регион у коме ћете користити ваше радио уређаје." + } + } + } }, "The root topic to use for MQTT." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Корен тема која ће се користити за MQTT." + } + } + } + }, + "The secondary public key authorized to send admin messages to this node." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Секундарни јавни кључ овлашћен за слање административних порука овом чвору." + } + } + } + }, + "The specified device has disconnected from us" : { + "extractionState" : "manual", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Наведени уређај је прекинуо везу са нама" + } + } + } }, "The state of the LED (on/off)" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стање LED диоде (укључено/искључено)" + } + } + } + }, + "The tertiary public key authorized to send admin messages to this node." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Терцијарни јавни кључ овлашћен за слање административних порука овом чвору." + } + } + } }, "There has been no response to a request for device metadata over the admin channel for this node." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није било одговора на захтев за метаподатке уређаја преко административног канала за овај чвор." + } + } + } }, "These settings will %@ channels. The current LoRa Config will be replaced, if there are substantial changes to the LoRa config the device will reboot" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ова подешавања ће %@ канале. Тренутна LoRA конфигурација ће бити замењена. Ако дође до значајних промена у LoRA конфигурацији, уређај ће се поново покренути." + } + } + } }, "Thirty Minutes" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dreißig Minuten" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тридесет минута" + } + } + } }, "This conversation will be deleted." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Овај разговор ће бити обрисан." + } + } + } + }, + "This could take a while, response will appear in the trace route log for the node it was sent to." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ово може потрајати. Одговор ће се појавити у евиденцији трасе праћења за чвор којем је послат." + } + } + } }, "This could take a while. The response will appear in the trace route log for the node it was sent to." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ово може потрајати. Одговор ће се појавити у евиденцији трасе праћења за чвор којем је послат." + } + } + } }, "This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ово одређује стварну фреквенцију на којој преносите у опсегу. Ако је постављено на 0, ова вредност ће се аутоматски израчунати на основу назива примарног канала." + } + } + } }, "This device will send out range test messages on the selected interval." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Овај уређај ће слати поруке за тестирање домета у одабраном интервалу." + } + } + } }, "This message was likely not delivered." : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Nachricht wurde höchstwahrscheinlich nicht übermittelt." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ова порука вероватно није била примљена." + } + } + } }, "This will disable fixed position and remove the currently set position." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ово ће онемогућити фиксну позицију и уклонити тренутно постављену позицију." + } + } + } }, "This will send a current position from your phone and enable fixed position." : { - - }, - "Tile Server" : { - - }, - "Tiles above Labels" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ово ће послати тренутну позицију са вашег телефона и омогућити фиксну позицију." + } + } + } }, "Time" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време" + } + } + } }, "Time Stamp" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitstempel" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временски жиг" + } + } + } }, "Time Zone" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitzone" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временска зона" + } + } + } }, "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." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временска зона за датуме на екрану уређаја и у евиденцији." + } + } + } }, "timeout" : { "localizations" : { @@ -21012,6 +29025,12 @@ "value" : "Tidsgräns överskriden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временско ограничење" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21031,7 +29050,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Timestamp" + "value" : "Zeitstempel" } }, "en" : { @@ -21070,6 +29089,12 @@ "value" : "Tidsstämpel" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временска ознака" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21085,7 +29110,14 @@ } }, "Timing & Format" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време и формат" + } + } + } }, "tip.bluetooth.connect.message" : { "localizations" : { @@ -21131,6 +29163,12 @@ "value" : "Visar information för LoRa-radion ansluten via bluetooth. Du kan svepa åt vänster för att koppla från radion och långtryck för att visa statistik eller starta liveaktivitet." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приказује информације за LoRA радио повезан преко Блутута. Можете превући лево да бисте одспојили радио и дуго притиснути да бисте погледали статистику или започели активност у реалном времену." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21189,6 +29227,12 @@ "value" : "Ansluten Radio" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Радио повезан" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21208,7 +29252,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" : { @@ -21247,6 +29291,12 @@ "value" : "Administratörskanal upptäckt: Välj en nod från rullgardinsmenyn för att hantera anslutna eller fjärranslutna enheter." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Детектован админ канал: Изаберите чвор из падајућег менија да бисте управљали повезаним или удаљеним уређајима." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21266,7 +29316,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Admin Channel" + "value" : "Admin Kanal" } }, "en" : { @@ -21305,6 +29355,12 @@ "value" : "Administratörskanal" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Административни канал" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21324,7 +29380,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" : { @@ -21363,6 +29419,12 @@ "value" : "De flesta data i ditt mesh-nätverk skickas över primärkanalen. Du kan ställa in sekundära kanaler för att skapa ytterligare meddelandegrupper skyddade av sin egen nyckel. Tips för kanalkonfiguration" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Већина података на вашој мрежи шаље се преко примарног канала. Можете подесити секундарне канале како бисте креирали додатне групе за размену порука, које су обезбеђене сопственим кључем. [Савети за конфигурацију канала](https://meshtastic.org/docs/configuration/tips/)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21382,7 +29444,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Manage Channels" + "value" : "Kanäle verwalten" } }, "en" : { @@ -21421,6 +29483,12 @@ "value" : "Hantera Kanaler" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управљај каналима" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21479,6 +29547,12 @@ "value" : "En Meshtastic QR-kod innehåller LoRa-konfigurationen och kanalvärden som behövs för kommunikation. De flesta aktiviteter i mesh-nätverket sker på den obligatoriska primärkanalen. Om du inte delar din primärkanal blir din första delade kanal primärkanalen på det andra nätverket. Andra kanaler är för privata grupper, varje med sin egen nyckel." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR код за Мештастик садржи LoRA конфигурацију и вредности канала које су потребне радијима за комуникацију. Можете поделити потпуну конфигурацију канала користећи опцију „Замени канале“, а ако изаберете „Додај канале“, ваши делени канали ће бити додати каналима на примајућем радију." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21498,7 +29572,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sharing Meshtastic Channels" + "value" : "Meshtastic Kanäle teilen" } }, "en" : { @@ -21537,6 +29611,12 @@ "value" : "Dela Meshtastic-kanaler" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дељење Мештастик канала" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21556,7 +29636,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" : { @@ -21595,6 +29675,12 @@ "value" : "Du kan skicka och ta emot kanalmeddelanden (gruppchatt) och direkta meddelanden. Från alla meddelanden kan du långtrycka för att se tillgängliga åtgärder som kopiera, svara, tapback och radera samt leveransdetaljer." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Можете слати и примати поруке у каналима (групним четовима) и директне поруке. Из било које поруке можете дуго притиснути да бисте видели доступне радње као што су копирање, одговор, реакција и брисање, као и детаље о испоруци." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21614,7 +29700,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Messages" + "value" : "Nachrichten" } }, "en" : { @@ -21653,6 +29739,12 @@ "value" : "Meddelanden" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поруке" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21668,40 +29760,199 @@ } }, "TLS Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TLS укључен" + } + } + } + }, + "Topic: %@" : { + "extractionState" : "manual", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тема: %@" + } + } + } }, "Total" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укупно" + } + } + } }, "Trace Route" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Праћење руте" + } + } + } }, "Trace Route Log" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лог праћења руте комуникације" + } + } + } }, - "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" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за праћење руте комуникације директно примљен од %1$@ са SNR од %2$@ dB." + } + } + } }, "Trace Route Sent" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за праћење руте комуникације послат." + } + } + } }, "Trace route sent to %@" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за праћење руте комуникације послат до %@." + } + } + } + }, + "Trace route to %@ was not sent." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захтев за праћење руте комуникације до %@ није послат." + } + } + } + }, + "Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Праћење руте комуникације је било ограничено по брзини. Можете послати захтев за праћење руте комуникације највише једном у сваких тридесет секунди." + } + } + } }, "Traffic" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verkehr" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Саобраћај" + } + } + } }, "Transmit data (txd) GPIO pin" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO pin за трансмисију података (txd)" + } + } + } }, "Transmit Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Трансмитер укључен" + } + } + } }, "Treat double tap on supported accelerometers as a user button press." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Третирај двоструки додир на подржаним акцелераметрима као притисак корисничког дугмета." + } + } + } + }, + "TriggerType" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тип покретача" + } + } + } + }, + "Triple Click Ad Hoc Ping" : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Троструки клик за Ad Hoc пинг" + } + } + } }, "Try Again" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erneut versuchen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Покушај поново" + } + } + } }, "twitter" : { "extractionState" : "manual", @@ -21748,6 +29999,12 @@ "value" : "Twitter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "X.com" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21763,13 +30020,34 @@ } }, "Two Hours" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Два сата" + } + } + } }, "Un-Favorite" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уклони са фаворита" + } + } + } }, "Units displayed on the device screen" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јединице приказане на екрану уређаја" + } + } + } }, "unknown" : { "localizations" : { @@ -21815,6 +30093,12 @@ "value" : "Okänd" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Непознато" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21873,6 +30157,12 @@ "value" : "Okänd ålder" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Непозната старост" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21931,6 +30221,12 @@ "value" : "Återställ" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уклони" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -21946,20 +30242,41 @@ } }, "Unsupported" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није подржано" + } + } + } }, "Up Down 1" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Горе Доле 1" + } + } + } }, "Update Interval" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал ажурирања" + } + } + } }, "update.firmware" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Update Your Firmware" + "value" : "Firmware aktualisieren" } }, "en" : { @@ -21998,6 +30315,12 @@ "value" : "Uppdatera din firmware" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажурирај твој фирмвер" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -22017,7 +30340,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Update intervall" + "value" : "Aktualisierungsintervall" } }, "en" : { @@ -22056,6 +30379,12 @@ "value" : "Uppdateringsintervall" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Интервал ажурирања" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -22070,14 +30399,41 @@ } } }, - "Updated Device Metrics Data." : { - + "Updated Node Stats Data." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажурирани подаци о статистици чвора." + } + } + } }, "Updated: %@" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualisiert: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ажуриран: %@" + } + } + } }, "Uplink Enabled" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Укључен узлазни канал" + } + } + } }, "uptime" : { "localizations" : { @@ -22104,20 +30460,64 @@ "state" : "translated", "value" : "Drifttid" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Време рада" + } } } }, "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." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи PWM излаз (као што је RAK звучник) за мелодије уместо укључивања/искључивања излаза. Ово ће игнорисати подешавања излаза, трајање излаза и активна подешавања и користити подешавање GPIO опције звучника у конфигурацији уређаја." + } + } + } }, "Use I2S As Buzzer" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи I2S као звучник" + } + } + } }, "Use Preset" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи предефинисано подешавање" + } + } + } }, "Use PWM Buzzer" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи PWM звучник" + } + } + } + }, + "Used to create a shared key with a remote device." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи се за креирање заједничког кључа са удаљеним уређајем." + } + } + } }, "user" : { "localizations" : { @@ -22163,6 +30563,12 @@ "value" : "Användare" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Корисник" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -22178,13 +30584,45 @@ } }, "User Config" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Корисничка подешавања" + } + } + } }, "User Details" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кориснички детаљи" + } + } + } }, "User Id" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ИД корисника" + } + } + } + }, + "User Initiated Disconnect" : { + "extractionState" : "manual", + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Корисник је покренуо прекид везе" + } + } + } }, "user.details" : { "extractionState" : "manual", @@ -22231,6 +30669,12 @@ "value" : "Användaruppgifter" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кориснички детаљи" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -22246,45 +30690,126 @@ } }, "Uses pullup resistor" : { - - }, - "Utilizes the network connection on your phone to connect to MQTT." : { - - }, - "Vehicle heading" : { - - }, - "Vehicle speed" : { - - }, - "Version %@ includes breaking changes to devices and the client apps. Only nodes version %@ and above are supported." : { "localizations" : { - "en" : { + "sr" : { "stringUnit" : { - "state" : "new", - "value" : "Version %1$@ includes breaking changes to devices and the client apps. Only nodes version %2$@ and above are supported." + "state" : "translated", + "value" : "Користи pull-up отпорник" } } } }, - "Version 1.2 End of life (EOL) Info" : { - + "Utilizes the network connection on your phone to connect to MQTT." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи мрежну везу на вашем телефону за повезивање са MQTT." + } + } + } + }, + "Vehicle heading" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahrzeugsteuerkurs" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Правац возила" + } + } + } + }, + "Vehicle speed" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahrzeuggeschwindigkeit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Брзина возила" + } + } + } + }, + "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 substantial network optimizations and extensive changes to devices and client apps. Only nodes version %2$@ and above are supported." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Верзија %1$@ укључује значајне оптимизације мреже и обимне измене уређаја и клијентских апликација. Подржане су само верзије чворова %2$@ и новије." + } + } + } }, "Version: %@ (%@) " : { "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version: %1$@ (%2$@) " + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Version: %1$@ (%2$@) " } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Верзија: %1$@ (%2$@) " + } } } }, "Via Lora" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via Lora" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преко LoRA" + } + } + } }, "Via Mqtt" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via Mqtt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преко MQTT-а" + } + } + } }, "voltage" : { "localizations" : { @@ -22330,6 +30855,12 @@ "value" : "Spänning" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Напон" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -22345,7 +30876,20 @@ } }, "Volts %@ " : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volt %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Волти %@" + } + } + } }, "waiting" : { "localizations" : { @@ -22391,6 +30935,12 @@ "value" : "Väntar..." } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чекам. . ." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -22406,43 +30956,202 @@ } }, "Waiting to be acknowledged. . ." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чека се на потврду пријема..." + } + } + } }, "Wake Screen on tap or motion" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пробуди екран додиром или покретом" + } + } + } }, "Waypoint Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wegpunktoptionen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције за тачке пута" + } + } + } }, "Weather Conditions" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wetterverhältnisse" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Временски услови" + } + } + } }, "Web Flasher" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Веб флашер" + } + } + } }, "Website" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вебсајт" + } + } + } + }, + "What does the lock mean?" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Was bedeutet das Schloß?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шта значи закључавање?" + } + } + } }, "What is Meshtastic?" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Was ist Meshtastic?" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шта је Мештастик?" + } + } + } }, "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" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шта ради режим лиценцираног оператера:\n* Поставља име чвора на ваш позивни знак\n* Емитује информације о чвору сваких 10 минута\n* Превазилази фреквенцију, циклус рада и излазну снагу\n* Онемогућава енкрипцију" + } + } + } }, "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)" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Када користите у GPIO режиму, задржите излаз укључен овако дуго." + } + } + } }, "WiFi Options" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFi Optionen" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опције вајфаја" + } + } + } }, "WIND" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "WIND" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ВЕТАР" + } + } + } + }, + "Wind Direction" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Windrichtung" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Правац ветра" + } + } + } + }, + "Wind Speed" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Windgeschwindigkeit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Брзина ветра" + } + } + } }, "x" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "x" + } + } + } }, "X: %@, Y: %d" : { "localizations" : { @@ -22451,6 +31160,12 @@ "state" : "new", "value" : "X: %1$@, Y: %2$d" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$d" + } } } }, @@ -22461,6 +31176,12 @@ "state" : "new", "value" : "X: %1$@, Y: %2$f" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$f" + } } } }, @@ -22471,38 +31192,126 @@ "state" : "new", "value" : "X: %1$@, Y: %2$lld" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$lld" + } } } }, "y" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "y" + } + } + } }, "Yesterday" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestern" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јуче" + } + } + } }, "You can also update your Meshtastic device over bluetooth using the Nordic DFU app." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Такође можете ажурирати свој Мештастик уређај преко блутута користећи Nordic DFU апликацију." + } + } + } }, "Your current location will be set as the fixed position and broadcast over the mesh on the position interval." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша тренутна позиција ће бити постављена као фиксна позиција и емитована преко мреже на интервалу позиције." + } + } + } }, "Your Firmware is up to date" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Firmware ist aktuell" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш фирмвер је на најновијој верзији" + } + } + } }, - "Your MQTT Server must support TLS." : { - + "Your MQTT Server must support TLS. Not available via the public mqtt server." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш MQTT сервер мора подржавати TLS. Није доступно преко јавног MQTT сервера." + } + } + } }, - "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." : { + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша позиција је послата са захтевом за одговор са њиховом позицијом. Добићете обавештење када се позиција врати." + } + } + } }, "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." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш регион има %lld%% циклус рада. MQTT се не препоручује када сте ограничени циклусом рада, јер ће додатни саобраћај брзо преоптеретити вашу LoRa мрежу." + } + } + } }, "Your region has a %lld%% hourly duty cycle, your radio will stop sending packets when it reaches the hourly limit." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш регион има %lld%% радни циклус по сату, ваш радио ће престати да шаље пакете када достигне ограничење по сату." + } + } + } }, "Your route file must have both Latitude and Longitude columns and headers." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша датотека руте мора имати колоне и заглавља и ширину и дужину." + } + } + } } }, "version" : "1.0" diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 49bf08ef..d767a83c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -32,7 +32,14 @@ 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 */; }; + 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 */; }; + 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 */; }; @@ -47,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 */; }; @@ -57,6 +65,9 @@ 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 */; }; + 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 */; }; @@ -90,6 +101,12 @@ 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 */; }; + 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 */; }; @@ -145,7 +162,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 */; }; @@ -199,6 +215,7 @@ DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */; }; DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; + DDDFE73F2D0D48FF0044463C /* IgnoreNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDFE73E2D0D48FF0044463C /* IgnoreNodeButton.swift */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */; }; DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; }; @@ -261,7 +278,14 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -277,6 +301,8 @@ 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 = ""; }; + 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 = ""; }; @@ -289,6 +315,10 @@ 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 = ""; }; + 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 = ""; }; @@ -298,6 +328,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 = ""; }; @@ -339,12 +370,21 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = ""; }; @@ -402,7 +442,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 = ""; }; @@ -468,6 +507,8 @@ DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshActivityAttributes.swift; sourceTree = ""; }; DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; + DDDFE73E2D0D48FF0044463C /* IgnoreNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IgnoreNodeButton.swift; sourceTree = ""; }; + DDDFE7402D0D4A070044463C /* MeshtasticDataModelV 47.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 47.xcdatamodel"; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteLog.swift; sourceTree = ""; }; DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = ""; }; @@ -517,6 +558,7 @@ 251926882C3BAF2E00249DF5 /* Actions */ = { isa = PBXGroup; children = ( + DDDFE73E2D0D48FF0044463C /* IgnoreNodeButton.swift */, 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */, 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */, 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */, @@ -544,6 +586,21 @@ path = MeshtasticTests; sourceTree = ""; }; + BCB6137F2C6728E700485544 /* AppIntents */ = { + isa = PBXGroup; + children = ( + BCB613802C67290800485544 /* SendWaypointIntent.swift */, + BCB613822C672A2600485544 /* MessageChannelIntent.swift */, + BCB613842C68703800485544 /* NodePositionIntent.swift */, + BCB613862C69A0FB00485544 /* AppIntentErrors.swift */, + BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */, + BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */, + BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */, + BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, + ); + path = AppIntents; + sourceTree = ""; + }; C9483F6B2773016700998F6B /* MapKitMap */ = { isa = PBXGroup; children = ( @@ -557,7 +614,6 @@ C9A7BC0E27759A6800760B50 /* Custom */ = { isa = PBXGroup; children = ( - C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */, DD964FC32974767D007C176F /* MapViewFitExtension.swift */, DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */, DDDB443529F6287000EE2349 /* MapButtons.swift */, @@ -593,6 +649,14 @@ path = CoreData; sourceTree = ""; }; + DD1BD0EC2C603C5B008C0C70 /* Measurement */ = { + isa = PBXGroup; + children = ( + DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */, + ); + path = Measurement; + sourceTree = ""; + }; DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( @@ -666,9 +730,10 @@ 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 */, ); path = Config; @@ -692,6 +757,24 @@ path = Module; sourceTree = ""; }; + DD6D5A312CA1176A00ED3032 /* Layouts */ = { + isa = PBXGroup; + children = ( + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */, + ); + path = Layouts; + sourceTree = ""; + }; + DD6F65772C6EAB860053C113 /* Help */ = { + isa = PBXGroup; + children = ( + DD6F65752C6EA5490053C113 /* AckErrors.swift */, + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */, + DD6F657A2C6EC2900053C113 /* LockLegend.swift */, + ); + path = Help; + sourceTree = ""; + }; DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( @@ -720,6 +803,7 @@ DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */, DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */, + DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */, DDB6ABDF28B13AC700384BA1 /* DeviceEnums.swift */, DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */, DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */, @@ -803,6 +887,8 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + BCB6137F2C6728E700485544 /* AppIntents */, + DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, DD7709392AA1ABA1007A8BF0 /* Tips */, DD90860A26F645B700DC5189 /* Meshtastic.entitlements */, @@ -834,6 +920,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + DD6D5A312CA1176A00ED3032 /* Layouts */, C9483F6B2773016700998F6B /* MapKitMap */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, @@ -883,9 +970,9 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + DD6F65772C6EAB860053C113 /* Help */, DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, - DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, @@ -897,6 +984,7 @@ DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */, DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */, DD5E523D298F5A7D00D21B61 /* Weather */, + DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, ); path = Helpers; sourceTree = ""; @@ -964,6 +1052,7 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */, DDFFA7462B3A7F3C004730DB /* Bundle.swift */, DDDB444529F8A96500EE2349 /* Character.swift */, + DD1BD0EA2C601795008C0C70 /* CLLocation.swift */, DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */, DDDB444B29F8AAA600EE2349 /* Color.swift */, 25C49D8F2C471AEA0024FBD1 /* Constants.swift */, @@ -982,6 +1071,7 @@ DDD5BB172C2F9C36007E03CA /* OSLogEntryLog.swift */, DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, DDD5BB0C2C285F00007E03CA /* Logger.swift */, + DD6F65732C6CB80A0053C113 /* View.swift */, ); path = Extensions; sourceTree = ""; @@ -1083,7 +1173,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1540; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1600; TargetAttributes = { 25F5D5C62C4375A8008036E3 = { CreatedOnToolsVersion = 15.4; @@ -1113,6 +1203,7 @@ "zh-Hant-TW", se, "pt-PT", + sr, ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( @@ -1201,6 +1292,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 */, @@ -1224,6 +1316,7 @@ 6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */, + DDDFE73F2D0D48FF0044463C /* IgnoreNodeButton.swift in Sources */, DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */, DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */, DD3D17E02C3FB67200561584 /* LocalWeatherConditions.swift in Sources */, @@ -1263,18 +1356,21 @@ 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 */, + DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, 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 */, 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 */, @@ -1302,10 +1398,13 @@ 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 */, 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 */, @@ -1324,6 +1423,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 */, @@ -1332,12 +1432,14 @@ 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 */, 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 */, @@ -1351,16 +1453,21 @@ 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 */, + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, + 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 */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, - C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, + BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, @@ -1370,6 +1477,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 */, @@ -1422,7 +1530,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1445,7 +1553,7 @@ DEVELOPMENT_TEAM = GCH7VS5Y9R; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1583,7 +1691,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; @@ -1598,12 +1705,12 @@ 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", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.5.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1618,7 +1725,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; @@ -1633,12 +1739,12 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.5.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1664,13 +1770,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.5.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1697,13 +1803,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.0; + MARKETING_VERSION = 2.5.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1815,6 +1921,13 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDDFE7402D0D4A070044463C /* MeshtasticDataModelV 47.xcdatamodel */, + DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */, + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */, + DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, + DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, + DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, + DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */, DD3D17DC2C3D7B1400561584 /* MeshtasticDataModelV 39.xcdatamodel */, DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */, @@ -1856,7 +1969,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */; + currentVersion = DDDFE7402D0D4A070044463C /* MeshtasticDataModelV 47.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; 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 { + // 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 new file mode 100644 index 00000000..aa9ea47a --- /dev/null +++ b/Meshtastic/AppIntents/MessageChannelIntent.swift @@ -0,0 +1,50 @@ +// +// MessageChannelIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/9/24. +// + +import Foundation +import AppIntents + +struct MessageChannelIntent: AppIntent { + static var title: LocalizedStringResource = "Send a Group Message" + + static var description: IntentDescription = "Send a message to a certain meshtastic channel" + + @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 { + 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 > 200 { + throw $messageContent.needsValueError("Message content exceeds 200 bytes.") + } + + 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..1e052eb9 --- /dev/null +++ b/Meshtastic/AppIntents/NodePositionIntent.swift @@ -0,0 +1,51 @@ +// +// 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/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift new file mode 100644 index 00000000..7ae8095a --- /dev/null +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -0,0 +1,39 @@ +// +// RestartNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/24/24. +// + +import Foundation +import AppIntents + +struct RestartNodeIntent: AppIntent { + 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 { + 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 new file mode 100644 index 00000000..4352c548 --- /dev/null +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -0,0 +1,85 @@ +// +// SendWaypointIntent.swift +// Meshtastic +// +// 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 { + 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") + } + + // 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/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift new file mode 100644 index 00000000..b21c7e7d --- /dev/null +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -0,0 +1,36 @@ +// +// 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 \(.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 \(.applicationName) node", + "Restart my \(.applicationName) node", + "Reboot \(.applicationName) node", + "Reboot my \(.applicationName) node"], + shortTitle: "Restart", + systemImageName: "arrow.circlepath") + + AppShortcut(intent: MessageChannelIntent(), + 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 new file mode 100644 index 00000000..dcb43f3c --- /dev/null +++ b/Meshtastic/AppIntents/ShutDownNodeIntent.swift @@ -0,0 +1,39 @@ +// +// ShutDownNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 8/24/24. +// + +import Foundation +import AppIntents + +struct ShutDownNodeIntent: AppIntent { + 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?")) + + 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() + } +} 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 00000000..9942a312 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-dark.png differ 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 00000000..85915643 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon.appiconset/logo-tinted.png differ diff --git a/Meshtastic/Assets.xcassets/TLORABOARD.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json similarity index 51% rename from Meshtastic/Assets.xcassets/TLORABOARD.imageset/Contents.json rename to Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json index f8356864..418dd7fe 100644 --- a/Meshtastic/Assets.xcassets/TLORABOARD.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/HELTECHT62.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "LILYGO-TTGO-LoRa32-V2-1-1-6-Version-433-868-915Mhz-ESP32-LoRa-OLED-0-96.jpg_Q90.jpg_.webp.png", + "filename" : "heltec-ht62-esp32c3-sx1262.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/HELTECHT62.imageset/heltec-ht62-esp32c3-sx1262.svg b/Meshtastic/Assets.xcassets/HELTECHT62.imageset/heltec-ht62-esp32c3-sx1262.svg new file mode 100644 index 00000000..c52534ef --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECHT62.imageset/heltec-ht62-esp32c3-sx1262.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json new file mode 100644 index 00000000..a4f550b7 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heltec-mesh-node-t114-case.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/heltec-mesh-node-t114-case.svg b/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/heltec-mesh-node-t114-case.svg new file mode 100644 index 00000000..b2abe639 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECMESHNODET114.imageset/heltec-mesh-node-t114-case.svg @@ -0,0 +1 @@ + \ 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 c4454bcc..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECV3.imageset/Heltec_turq.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/HELTECV3.imageset/heltec-v3-case.svg b/Meshtastic/Assets.xcassets/HELTECV3.imageset/heltec-v3-case.svg new file mode 100644 index 00000000..1b1d3c55 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECV3.imageset/heltec-v3-case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json new file mode 100644 index 00000000..687a7da9 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heltec-vision-master-e213.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/heltec-vision-master-e213.svg b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/heltec-vision-master-e213.svg new file mode 100644 index 00000000..2c1cca09 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE213.imageset/heltec-vision-master-e213.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json new file mode 100644 index 00000000..13ddda16 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heltec-vision-master-e290.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/heltec-vision-master-e290.svg b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/heltec-vision-master-e290.svg new file mode 100644 index 00000000..ca7d296a --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECVISIONMASTERE290.imageset/heltec-vision-master-e290.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json index 1a8d07dc..a1a7444e 100644 --- a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Paper-Meshtastic-2 copy.jpg", + "filename" : "heltec-wireless-paper.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Paper-Meshtastic-2 copy.jpg b/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Paper-Meshtastic-2 copy.jpg deleted file mode 100644 index 36692599..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/Paper-Meshtastic-2 copy.jpg and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/heltec-wireless-paper.svg b/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/heltec-wireless-paper.svg new file mode 100644 index 00000000..cb3f188d --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECWIRELESSPAPER.imageset/heltec-wireless-paper.svg @@ -0,0 +1 @@ + diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json index 3b6b227c..d13152fe 100644 --- a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "images.png", + "filename" : "heltec-wireless-tracker.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/heltec-wireless-tracker.svg b/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/heltec-wireless-tracker.svg new file mode 100644 index 00000000..a5392595 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/heltec-wireless-tracker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/images.png b/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/images.png deleted file mode 100644 index 4e9336c5..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECWIRELESSTRACKER.imageset/images.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json index aed717e4..dea94fc1 100644 --- a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "heltecwsl.png", + "filename" : "heltec-wsl-v3.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltec-wsl-v3.svg b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltec-wsl-v3.svg new file mode 100644 index 00000000..1741223e --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltec-wsl-v3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl.png deleted file mode 100644 index 8881d0e1..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json index 892d20eb..1febc627 100644 --- a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "tbeam_supreme.png", + "filename" : "tbeam-s3-core.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam-s3-core.svg b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam-s3-core.svg new file mode 100644 index 00000000..f42e6d2c --- /dev/null +++ b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam-s3-core.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.png b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.png deleted file mode 100644 index 6a618653..00000000 Binary files a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json b/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json index 2f074381..fe8b1d15 100644 --- a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "nano_g2_ultra_product_image.jpg", + "filename" : "nano-g2-ultra.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano-g2-ultra.svg b/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano-g2-ultra.svg new file mode 100644 index 00000000..6dbe47af --- /dev/null +++ b/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano-g2-ultra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano_g2_ultra_product_image.jpg b/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano_g2_ultra_product_image.jpg deleted file mode 100644 index 18f2b472..00000000 Binary files a/Meshtastic/Assets.xcassets/NANOG2ULTRA.imageset/nano_g2_ultra_product_image.jpg and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/RAK11200.imageset/Contents.json b/Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json similarity index 75% rename from Meshtastic/Assets.xcassets/RAK11200.imageset/Contents.json rename to Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json index ed6c2585..0fbd5109 100644 --- a/Meshtastic/Assets.xcassets/RAK11200.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/PROMICRO.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "RAK_DEV_KIT-2.jpg", + "filename" : "promicro.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/PROMICRO.imageset/promicro.svg b/Meshtastic/Assets.xcassets/PROMICRO.imageset/promicro.svg new file mode 100644 index 00000000..3dc26021 --- /dev/null +++ b/Meshtastic/Assets.xcassets/PROMICRO.imageset/promicro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/RAK11200.imageset/RAK_DEV_KIT-2.jpg b/Meshtastic/Assets.xcassets/RAK11200.imageset/RAK_DEV_KIT-2.jpg deleted file mode 100644 index 9300bed0..00000000 Binary files a/Meshtastic/Assets.xcassets/RAK11200.imageset/RAK_DEV_KIT-2.jpg and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/TLORAV1.imageset/Contents.json b/Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json similarity index 75% rename from Meshtastic/Assets.xcassets/TLORAV1.imageset/Contents.json rename to Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json index 093c722d..3046b536 100644 --- a/Meshtastic/Assets.xcassets/TLORAV1.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/RAK11310.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "TLORA_olive 1.png", + "filename" : "rak11310.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/RAK11310.imageset/rak11310.svg b/Meshtastic/Assets.xcassets/RAK11310.imageset/rak11310.svg new file mode 100644 index 00000000..8c5ce28e --- /dev/null +++ b/Meshtastic/Assets.xcassets/RAK11310.imageset/rak11310.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json b/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json index feb2e6c0..60b17db3 100644 --- a/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/RAK4631.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "RAK 4.png", + "filename" : "rak4631_case.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/RAK4631.imageset/RAK 4.png b/Meshtastic/Assets.xcassets/RAK4631.imageset/RAK 4.png deleted file mode 100644 index e34322b8..00000000 Binary files a/Meshtastic/Assets.xcassets/RAK4631.imageset/RAK 4.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/RAK4631.imageset/rak4631_case.svg b/Meshtastic/Assets.xcassets/RAK4631.imageset/rak4631_case.svg new file mode 100644 index 00000000..a0b2bbb8 --- /dev/null +++ b/Meshtastic/Assets.xcassets/RAK4631.imageset/rak4631_case.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json b/Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json new file mode 100644 index 00000000..87088506 --- /dev/null +++ b/Meshtastic/Assets.xcassets/RPIPICO.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pico.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/RPIPICO.imageset/pico.svg b/Meshtastic/Assets.xcassets/RPIPICO.imageset/pico.svg new file mode 100644 index 00000000..82ce6526 --- /dev/null +++ b/Meshtastic/Assets.xcassets/RPIPICO.imageset/pico.svg @@ -0,0 +1,2956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json new file mode 100644 index 00000000..fdd4019e --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "seeed-xiao-s3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/seeed-xiao-s3.svg b/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/seeed-xiao-s3.svg new file mode 100644 index 00000000..04e97fe0 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDXIAOS3.imageset/seeed-xiao-s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json b/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json new file mode 100644 index 00000000..3870939e --- /dev/null +++ b/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "seeed-sensecap-indicator.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/seeed-sensecap-indicator.svg b/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/seeed-sensecap-indicator.svg new file mode 100644 index 00000000..f7bf9db0 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SENSECAPINDICATOR.imageset/seeed-sensecap-indicator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json b/Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json new file mode 100644 index 00000000..dc823045 --- /dev/null +++ b/Meshtastic/Assets.xcassets/STATIONG2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "station-g2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/STATIONG2.imageset/station-g2.svg b/Meshtastic/Assets.xcassets/STATIONG2.imageset/station-g2.svg new file mode 100644 index 00000000..8d2e0aed --- /dev/null +++ b/Meshtastic/Assets.xcassets/STATIONG2.imageset/station-g2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json b/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json index 64a09f22..0ecd041c 100644 --- a/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/TBEAM.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "tbeam.png", + "filename" : "tbeam.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.png b/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.png deleted file mode 100644 index 75fec7be..00000000 Binary files a/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.svg b/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.svg new file mode 100644 index 00000000..cd0475c6 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TBEAM.imageset/tbeam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json b/Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json new file mode 100644 index 00000000..b8451344 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TDECK.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "t-deck.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TDECK.imageset/t-deck.svg b/Meshtastic/Assets.xcassets/TDECK.imageset/t-deck.svg new file mode 100644 index 00000000..cdc53c5d --- /dev/null +++ b/Meshtastic/Assets.xcassets/TDECK.imageset/t-deck.svg @@ -0,0 +1 @@ +QWERTYIUPOASDFGHKJLaltZXCVBMN \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json b/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json index f380b7af..e1adcf61 100644 --- a/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/TECHO.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "LILYGO-TTGO-SoftRF-T-Echo-NRF52840-LoRa-SX1262-433-868-915MHz-Wireless-Module-L76K-GPS-1.png", + "filename" : "t-echo.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/TECHO.imageset/LILYGO-TTGO-SoftRF-T-Echo-NRF52840-LoRa-SX1262-433-868-915MHz-Wireless-Module-L76K-GPS-1.png b/Meshtastic/Assets.xcassets/TECHO.imageset/LILYGO-TTGO-SoftRF-T-Echo-NRF52840-LoRa-SX1262-433-868-915MHz-Wireless-Module-L76K-GPS-1.png deleted file mode 100644 index 7b2f9f96..00000000 Binary files a/Meshtastic/Assets.xcassets/TECHO.imageset/LILYGO-TTGO-SoftRF-T-Echo-NRF52840-LoRa-SX1262-433-868-915MHz-Wireless-Module-L76K-GPS-1.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/TECHO.imageset/t-echo.svg b/Meshtastic/Assets.xcassets/TECHO.imageset/t-echo.svg new file mode 100644 index 00000000..e178a50f --- /dev/null +++ b/Meshtastic/Assets.xcassets/TECHO.imageset/t-echo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/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 b/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 deleted file mode 100644 index ff3da639..00000000 Binary files a/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 and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json new file mode 100644 index 00000000..593dc16e --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAC6.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tlora-c6.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TLORAC6.imageset/tlora-c6.svg b/Meshtastic/Assets.xcassets/TLORAC6.imageset/tlora-c6.svg new file mode 100644 index 00000000..8b626638 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAC6.imageset/tlora-c6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json new file mode 100644 index 00000000..33fb9c78 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tlora-t3s3-epaper.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/tlora-t3s3-epaper.svg b/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/tlora-t3s3-epaper.svg new file mode 100644 index 00000000..6f2e8452 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAT3S3EPAPER.imageset/tlora-t3s3-epaper.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json new file mode 100644 index 00000000..a5716fc8 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tlora-t3s3-v1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/tlora-t3s3-v1.svg b/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/tlora-t3s3-v1.svg new file mode 100644 index 00000000..1f8847d4 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAT3S3V1.imageset/tlora-t3s3-v1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TLORAV1.imageset/TLORA_olive 1.png b/Meshtastic/Assets.xcassets/TLORAV1.imageset/TLORA_olive 1.png deleted file mode 100644 index e8980a2c..00000000 Binary files a/Meshtastic/Assets.xcassets/TLORAV1.imageset/TLORA_olive 1.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/TLORAV2116.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAV2116.imageset/Contents.json new file mode 100644 index 00000000..eb286609 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAV2116.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tlora-v2-1-1_6.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TLORAV2116.imageset/tlora-v2-1-1_6.svg b/Meshtastic/Assets.xcassets/TLORAV2116.imageset/tlora-v2-1-1_6.svg new file mode 100644 index 00000000..dbe36ef5 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAV2116.imageset/tlora-v2-1-1_6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TLORAV2118.imageset/Contents.json b/Meshtastic/Assets.xcassets/TLORAV2118.imageset/Contents.json new file mode 100644 index 00000000..c7aff831 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAV2118.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tlora-v2-1-1_8.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TLORAV2118.imageset/tlora-v2-1-1_8.svg b/Meshtastic/Assets.xcassets/TLORAV2118.imageset/tlora-v2-1-1_8.svg new file mode 100644 index 00000000..dbe36ef5 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TLORAV2118.imageset/tlora-v2-1-1_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json b/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json new file mode 100644 index 00000000..e966c95f --- /dev/null +++ b/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tracker-t1000-e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/tracker-t1000-e.svg b/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/tracker-t1000-e.svg new file mode 100644 index 00000000..6f7a06c9 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TRACKERT1000E.imageset/tracker-t1000-e.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json b/Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json new file mode 100644 index 00000000..baffc648 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TWATCHS3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "t-watch-s3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/TWATCHS3.imageset/t-watch-s3.svg b/Meshtastic/Assets.xcassets/TWATCHS3.imageset/t-watch-s3.svg new file mode 100644 index 00000000..19084c19 --- /dev/null +++ b/Meshtastic/Assets.xcassets/TWATCHS3.imageset/t-watch-s3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/UNSET.imageset/Contents.json b/Meshtastic/Assets.xcassets/UNSET.imageset/Contents.json index 04be44d5..4508d9cd 100644 --- a/Meshtastic/Assets.xcassets/UNSET.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/UNSET.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "play_store_icon_114px-2.png", + "filename" : "unknown.svg", "idiom" : "universal" } ], diff --git a/Meshtastic/Assets.xcassets/UNSET.imageset/play_store_icon_114px-2.png b/Meshtastic/Assets.xcassets/UNSET.imageset/play_store_icon_114px-2.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/UNSET.imageset/play_store_icon_114px-2.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg b/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg new file mode 100644 index 00000000..3b0a0744 --- /dev/null +++ b/Meshtastic/Assets.xcassets/UNSET.imageset/unknown.svg @@ -0,0 +1,129 @@ + + diff --git a/Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json b/Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json new file mode 100644 index 00000000..706f7fc3 --- /dev/null +++ b/Meshtastic/Assets.xcassets/WIOWM1110.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wio-tracker-wm1110.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/WIOWM1110.imageset/wio-tracker-wm1110.svg b/Meshtastic/Assets.xcassets/WIOWM1110.imageset/wio-tracker-wm1110.svg new file mode 100644 index 00000000..15ace5c5 --- /dev/null +++ b/Meshtastic/Assets.xcassets/WIOWM1110.imageset/wio-tracker-wm1110.svg @@ -0,0 +1 @@ +LoRaWI FILEDRESETGNSSBLE \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json b/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json new file mode 100644 index 00000000..85d43a9b --- /dev/null +++ b/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "rak-wismeshtap.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/rak-wismeshtap.svg b/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/rak-wismeshtap.svg new file mode 100644 index 00000000..34e77876 --- /dev/null +++ b/Meshtastic/Assets.xcassets/WISMESHTAP.imageset/rak-wismeshtap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index bfd8ce9e..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 @@ -59,7 +63,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/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/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index 5c980da0..2128fafa 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -27,27 +27,27 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { var name: String { switch self { case .client: - return "Client" + return "device.role.name.client".localized case .clientMute: - return "Client Mute" + return "device.role.name.clientMute".localized case .router: - return "Router" + return "device.role.name.router".localized case .routerClient: - return "Router & Client" + return "device.role.name.routerClient".localized case .repeater: - return "Repeater" + return "device.role.name.repeater".localized case .tracker: - return "Tracker" + return "device.role.name.tracker".localized case .sensor: - return "Sensor" + return "device.role.name.sensor".localized case .tak: - return "TAK" + return "device.role.name.tak".localized case .takTracker: - return "TAK Tracker" + return "device.role.name.takTracker".localized case .clientHidden: - return "Client Hidden" + return "device.role.name.clientHidden".localized case .lostAndFound: - return "Lost and Found" + return "device.role.name.lostAndFound".localized } } diff --git a/Meshtastic/Enums/DisplayEnums.swift b/Meshtastic/Enums/DisplayEnums.swift index a540a9d2..8959668a 100644 --- a/Meshtastic/Enums/DisplayEnums.swift +++ b/Meshtastic/Enums/DisplayEnums.swift @@ -149,13 +149,13 @@ enum DisplayModes: Int, CaseIterable, Identifiable { var description: String { switch self { case .defaultMode: - return "Default 128x64 screen layout" + return "default.128x64.screen.layout".localized case .twoColor: - return "Optimized for 2 color displays" + return "optimized.for.2.color.displays".localized case .inverted: - return "Inverted top bar for 2 Color display" + return "inverted.top.bar.for.2.color.display".localized case .color: - return "TFT Full Color Displays" + return "tft.full.color.displays".localized } } func protoEnumValue() -> Config.DisplayConfig.DisplayMode { diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 7da6268e..deccde0f 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,12 +162,66 @@ 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 + } + } + var isCountry: Bool { + switch self { + case .unset: + return false + case .us: + return true + case .eu433: + return false + case .eu868: + return false + case .cn: + return true + case .jp: + return true + case .anz: + return false + case .kr: + return true + case .tw: + return true + case .ru: + return true + case .in: + return true + case .nz865: + return true + case .th: + return true + case .ua433: + return true + case .ua868: + return true + case .lora24: + return false + case .my433: + return true + case .my919: + return true + case .sg923: + return true + case .ph433: + return true + case .ph868: + return true + case .ph915: + return true } } func protoEnumValue() -> Config.LoRaConfig.RegionCode { @@ -190,12 +259,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 } } } @@ -210,6 +285,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 +306,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 +328,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "ShortSlow" case .shortFast: return "ShortFast" + case .shortTurbo: + return "ShortTurbo" } } func snrLimit() -> Float { @@ -270,6 +350,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 +372,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 } } } diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 0773265b..b3962a09 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 { @@ -21,6 +22,10 @@ enum RoutingError: Int, CaseIterable, Identifiable { case dutyCycleLimit = 9 case badRequest = 32 case notAuthorized = 33 + case pkiFailed = 34 + case pkiUnknownPubkey = 35 + case adminBadSessionKey = 36 + case adminPublicKeyUnauthorized = 37 var id: Int { self.rawValue } var display: String { @@ -50,6 +55,59 @@ enum RoutingError: Int, CaseIterable, Identifiable { return "routing.badRequest".localized case .notAuthorized: return "routing.notauthorized".localized + case .pkiFailed: + 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 { + if self == .none { + return Color.secondary + } else if self.canRetry { + return Color.orange + } else { + return Color.red + } + } + var canRetry: Bool { + switch self { + case .none: + return false + case .noRoute: + return true + case .gotNak: + return true + case .timeout: + return true + case .noInterface: + return true + case .maxRetransmit: + return true + case .noChannel: + return true + case .tooLarge: + return false + case .noResponse: + return true + case .dutyCycleLimit: + return true + case .badRequest: + return true + case .notAuthorized: + return true + case .pkiFailed: + return true + case .pkiUnknownPubkey: + return true + case .adminBadSessionKey: + return true + case .adminPublicKeyUnauthorized: + return true } } func protoEnumValue() -> Routing.Error { @@ -80,7 +138,14 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Routing.Error.badRequest case .notAuthorized: return Routing.Error.notAuthorized - + case .pkiFailed: + return Routing.Error.pkiFailed + case .pkiUnknownPubkey: + return Routing.Error.pkiUnknownPubkey + case .adminBadSessionKey: + return Routing.Error.adminBadSessionKey + case .adminPublicKeyUnauthorized: + return Routing.Error.adminPublicKeyUnauthorized } } } diff --git a/Meshtastic/Enums/TelemetryEnums.swift b/Meshtastic/Enums/TelemetryEnums.swift index 187a8d74..68d65961 100644 --- a/Meshtastic/Enums/TelemetryEnums.swift +++ b/Meshtastic/Enums/TelemetryEnums.swift @@ -20,17 +20,17 @@ enum Aqi: Int, CaseIterable, Identifiable { var description: String { switch self { case .good: - return "Good" + return "telemetry.good".localized case .moderate: - return "Moderate" + return "telemetry.moderate".localized case .sensitive: - return "Unhealthy for Sensitive Groups" + return "telemetry.sensitive".localized case .unhealthy: - return "Unhealthy" + return "telementry.unhealthy".localized case .veryUnhealthy: - return "Very Unhealthy" + return "telementry.veryUnhealthy".localized case .hazardous: - return "Hazardous" + return "telementry.hazardous".localized } } var color: Color { @@ -176,3 +176,29 @@ 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/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/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 62f7eff0..57babf4a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -11,8 +11,12 @@ import MeshtasticProtobufs 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) - 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..70f36c3d 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -14,11 +14,21 @@ import SwiftUI extension MessageEntity { var timestamp: Date { - let time = messageTimestamp <= 0 ? receivedTimestamp : messageTimestamp + let time = messageTimestamp return Date(timeIntervalSince1970: TimeInterval(time)) } var canRetry: Bool { - return ackError == 9 || ackError == 5 || ackError == 3 + let re = RoutingError(rawValue: Int(ackError)) + return re?.canRetry ?? false + } + + 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 80ba19c8..68a48ee1 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -10,13 +10,19 @@ 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.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] + fetchRequest.predicate = NSPredicate(format: "toUser == nil") + + return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } var unreadMessages: Int { 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/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index e88a2bee..7d313191 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -14,12 +14,16 @@ 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 } var hasPositions: Bool { - return positions?.count ?? 0 > 0 + return self.positions?.count ?? 0 > 0 } var hasDeviceMetrics: Bool { @@ -36,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 { @@ -48,12 +53,21 @@ 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 } + + var canRemoteAdmin: Bool { + if UserDefaults.enableAdministration { + 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 { @@ -63,7 +77,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/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/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index a702fb6d..57681fd7 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -12,59 +12,100 @@ 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.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]() } var sensorMessageList: [MessageEntity] { - self.value(forKey: "detectionSensorMessages") as? [MessageEntity] ?? [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]() } var unreadMessages: Int { let unreadMessages = messageList.filter { ($0 as AnyObject).read == false } return unreadMessages.count } - + /// SVG Images for Vendors who are signed project backers var hardwareImage: String? { guard let hwModel else { return nil } switch hwModel { - case "HELTECV1", "HELTECV3", "HELTECV20", "HELTECV21": + /// Heltec + case "HELTECHT62": + return "HELTECHT62" + case "HELTECMESHNODET114": + return "HELTECMESHNODET114" + case "HELTECV3": return "HELTECV3" + case "HELTECVISIONMASTERE213": + return "HELTECVISIONMASTERE213" + case "HELTECVISIONMASTERE290": + return "HELTECVISIONMASTERE290" case "HELTECWIRELESSPAPER", "HELTECWIRELESSPAPERV10": return "HELTECWIRELESSPAPER" case "HELTECWIRELESSTRACKER", "HELTECWIRELESSTRACKERV10": return "HELTECWIRELESSTRACKER" case "HELTECWSLV3": return "HELTECWSLV3" - case "LILYGOTBEAMSCORE": + /// LilyGO + case "TDECK": + return "TDECK" + case "TECHO": + return "TECHO" + case "TWATCHS3": + return "TWATCHS3" + case "LILYGOTBEAMS3CORE": return "LILYGOTBEAMS3CORE" + case "TBEAM", "TBEAM_V0P7": + return "TBEAM" + case "TLORAC6": + return "TLORAC6" + case "TLORAT3S3EPAPER": + return "TLORAT3S3EPAPER" + case "TLORAT3S3V1": + return "TLORAT3S3V1" + case "TLORAV2116": + return "TLORAV2116" + case "TLORAV2118": + return "TLORAV2118" + /// Seeed Studio + case "SENSECAPINDICATOR": + return "SENSECAPINDICATOR" + case "TRACKERT1000E": + return "TRACKERT1000E" + case "SEEEDXIAOS3": + return "SEEEDXIAOS3" + case "WIOWM1110": + return "WIOWM1110" + /// RAK Wireless + case "RAK4631": + return "RAK4631" + case "RAK11310": + return "RAK11310" + case "WISMESHTAP": + return "WISMESHTAP" + /// B&Q Consulting case "NANOG1", "NANOG1EXPLORER": return "NANOG1" case "NANOG2ULTRA": return "NANOG2ULTRA" - case "RAK4631": - return "RAK4631" - case "RAK11200": - return "RAK11200" - case "SOLAR_NODE": - return "SOLAR_NODE" - case "STATIONG1": - return "STATIONG1" - case "ТВЕАМ", "TBEAMVOP7": - return "ТВЕАМ" - case "TECHO": - return "TECHO" - case "TLORAV1", "TLORAV11P3": - return "TLORAV1" - case "TLORAV2", "TLORAT3S3", "TLORAV211P6", "TLORAV211P8": - return "TLORABOARD" - case "UNPHONE": - return "UNPHONE" + case "STATIONG2": + return "STATIONG2" + /// DIY Devices + case "RPIPICO": + return "RPIPICO" default: return "UNSET" } } } - public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity { let newUser = UserEntity(context: context) newUser.num = Int64(num) 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/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6255b151..b97ad1c5 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -61,11 +61,10 @@ 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) } var length: Int { @@ -91,5 +90,4 @@ extension String { let end = index(start, offsetBy: range.upperBound - range.lowerBound) return String(self[start ..< end]) } - } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 11049bfb..740f04e2 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 @@ -72,6 +71,7 @@ extension UserDefaults { case modemPreset case firmwareVersion case environmentEnableWeatherKit + case enableAdministration case testIntEnum } @@ -121,9 +121,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 @@ -162,10 +159,13 @@ extension UserDefaults { @UserDefault(.firmwareVersion, defaultValue: "0.0.0") static var firmwareVersion: String - + @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/Extensions/View.swift b/Meshtastic/Extensions/View.swift new file mode 100644 index 00000000..cec5b003 --- /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 () -> Void) -> some View { + modifier(FirstAppear(action: action)) + } +} + +private struct FirstAppear: ViewModifier { + let action: () -> Void + + // 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/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5d6cceb8..5484f74a 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 @@ -26,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.3.2" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false @@ -53,20 +54,27 @@ 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") + } - init( - appState: AppState, - context: NSManagedObjectContext - ) { - self.appState = appState - self.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) + } - 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]) + 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 @@ -234,14 +242,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly. // Happens when device is manually reset / powered off lastConnectionError = "🚨" + String.localizedStringWithFormat("ble.errorcode.6 %@".localized, e.localizedDescription) - Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown", privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") + Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } else if errorCode == 7 { // CBError.Code.peripheralDisconnected The specified device has disconnected from us. // Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work. if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString { manager.notifications = [ Notification( id: (peripheral.identifier.uuidString), - title: "Radio Disconnected", + title: "Radio Disconnected".localized, subtitle: "\(peripheral.name ?? "unknown".localized)", content: e.localizedDescription, target: "bluetooth", @@ -250,18 +258,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ] manager.schedule() } - lastConnectionError = "🚨 \(e.localizedDescription)" - Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown", privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") + lastConnectionError = "🚨 \("The specified device has disconnected from us".localized)" + Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } else if errorCode == 14 { // Peer removed pairing information // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that lastConnectionError = "🚨 " + String.localizedStringWithFormat("ble.errorcode.14 %@".localized, e.localizedDescription) - Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)") + Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)") } else { if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString { manager.notifications = [ Notification( id: (peripheral.identifier.uuidString), - title: "Radio Disconnected", + title: "Radio Disconnected".localized, subtitle: "\(peripheral.name ?? "unknown".localized)", content: e.localizedDescription, target: "bluetooth", @@ -271,12 +279,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate manager.schedule() } lastConnectionError = "🚨 \(e.localizedDescription)" - Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown", privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") + Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } } else { // Disconnected without error which indicates user intent to disconnect // Happens when swiping to disconnect - Logger.services.info("ℹ️ [BLE] Disconnected: \(peripheral.name ?? "Unknown", privacy: .public): User Initiated Disconnect") + Logger.services.info("ℹ️ [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public): \(String(describing: "User Initiated Disconnect".localized))") } // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() @@ -453,19 +461,9 @@ 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 - // 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)") @@ -590,7 +588,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: @@ -611,14 +609,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 { @@ -627,7 +617,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)") @@ -641,6 +631,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate retained: decodedInfo.mqttClientProxyMessage.retained ) 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( + 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 { @@ -669,12 +688,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)") } @@ -814,63 +831,148 @@ 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 if routingMessage.route.count == 0 { - let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from)) + // 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)") - } else { - var routeString = "You --> " + guard let connectedNode = getNodeInfo(id: Int64(connectedPeripheral.num), context: context) else { + return + } var hopNodes: [TraceRouteHopEntity] = [] - for node in routingMessage.route { + let connectedHop = TraceRouteHopEntity(context: context) + connectedHop.time = Date() + connectedHop.num = connectedPeripheral.num + connectedHop.name = connectedNode.user?.longName ?? "???" + // If nil, set to unknown, INT8_MIN (-128) then divide by 4 + connectedHop.snr = Float(routingMessage.snrBack.last ?? -128) / 4 + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + connectedHop.altitude = mostRecent.altitude + connectedHop.latitudeI = mostRecent.latitudeI + connectedHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + var routeString = "\(connectedNode.user?.longName ?? "???") --> " + 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 { + if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() - 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 routingMessage.snrTowards.count >= index + 1 { + traceRouteHop.snr = Float(routingMessage.snrTowards[index]) / 4 + } else { + // 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())! { traceRouteHop.altitude = mostRecent.altitude 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 { if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } - hopNodes.append(traceRouteHop) } - routeString += "\(hopNode?.user?.longName ?? "unknown".localized) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") --> " + hopNodes.append(traceRouteHop) + + 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) --> " } - routeString += traceRoute?.node?.user?.longName ?? "unknown".localized + let destinationHop = TraceRouteHopEntity(context: context) + destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized + destinationHop.time = Date() + // If nil, set to unknown, INT8_MIN (-128) then divide by 4 + destinationHop.snr = Float(routingMessage.snrTowards.last ?? -128) / 4 + destinationHop.num = traceRoute?.node?.num ?? 0 + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + destinationHop.altitude = mostRecent.altitude + destinationHop.latitudeI = mostRecent.latitudeI + destinationHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + hopNodes.append(destinationHop) + /// 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 + + 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) + + 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 + 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 { try context.save() Logger.data.info("💾 Saved Trace Route") } 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)") } } 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)") } @@ -886,6 +988,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .powerstressApp: MeshLogger.log("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + case .alertApp: + MeshLogger.log("🕸️ MESH PACKET received for Alert App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { @@ -893,6 +997,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)") + if sendTime() { + } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again @@ -902,7 +1008,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 { @@ -1000,6 +1106,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 @@ -1022,6 +1132,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) @@ -1132,48 +1246,40 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - @MainActor - public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { + @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 - } - 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) + guard let lastLocation = LocationsHandler.shared.locationsArray.last else { + return nil + } - 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) - } + 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) - } + /// From GPS, but time can be old and have drifted + positionPacket.locationSource = Position.LocSource.locManual } return positionPacket } @@ -1181,7 +1287,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 @@ -1236,7 +1342,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 } @@ -1265,7 +1371,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 @@ -1286,9 +1391,37 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } + public func sendTime() -> Bool { + var adminPacket = AdminMessage() + adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(self.connectedPeripheral.num) + meshPacket.from = UInt32(self.connectedPeripheral.num) + 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) @@ -1314,6 +1447,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) @@ -1339,6 +1475,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) @@ -1364,6 +1503,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) @@ -1389,7 +1531,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() - adminPacket.factoryReset = 5 + 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) @@ -1415,6 +1560,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) @@ -1516,14 +1664,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] @@ -1627,6 +1775,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) @@ -1746,9 +1897,70 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func setIgnoredNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool { + var adminPacket = AdminMessage() + adminPacket.setIgnoredNode = UInt32(node.num) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedNodeNum) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + var adminPacket = AdminMessage() + adminPacket.removeIgnoredNode = UInt32(node.num) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedNodeNum) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. 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) @@ -1772,6 +1984,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) @@ -1789,7 +2004,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) } @@ -1800,7 +2015,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) @@ -1817,7 +2034,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 @@ -1826,6 +2043,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) @@ -1844,7 +2064,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 @@ -1854,6 +2074,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) @@ -1871,10 +2094,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, context: context) + upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } - return 0 } @@ -1882,7 +2104,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) @@ -1944,7 +2168,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) @@ -1971,11 +2197,46 @@ 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 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } + 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() 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) @@ -2005,7 +2266,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) @@ -2035,7 +2298,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) @@ -2066,7 +2331,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).. Bool { + + var adminPacket = AdminMessage() + adminPacket.getConfigRequest = AdminMessage.ConfigType.securityConfig + 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() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2650,7 +2957,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2680,7 +2986,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2710,7 +3015,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2740,7 +3044,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2770,7 +3073,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2800,7 +3102,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2830,7 +3131,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2860,7 +3160,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2890,7 +3189,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2920,7 +3218,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig - + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -3001,7 +3299,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: @@ -3139,7 +3437,6 @@ extension BLEManager: CBCentralManagerDelegate { } var status = "" - switch central.state { case .poweredOff: status = "BLE is powered off" @@ -3161,7 +3458,6 @@ extension BLEManager: CBCentralManagerDelegate { // 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)") @@ -3169,7 +3465,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 3e41c8f3..47f64bce 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -5,6 +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: []) // Step 1 Request Permissions for notifications private func requestAuthorization() { @@ -31,6 +34,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 +57,16 @@ 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 +98,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/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 9b16e3de..a215667b 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -10,7 +10,6 @@ import CoreLocation import OSLog // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. -@available(iOS 17.0, macOS 14.0, *) @MainActor class LocationsHandler: ObservableObject { static let shared = LocationsHandler() // Create a single, shared instance of the object. @@ -85,15 +84,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/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 5ee37673..fd9ffb8e 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) } } @@ -196,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()) @@ -230,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 { @@ -264,6 +270,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.num = Int64(nodeInfo.num) newNode.channel = Int32(nodeInfo.channel) newNode.favorite = nodeInfo.isFavorite + newNode.ignored = nodeInfo.isIgnored newNode.hopsAway = Int32(nodeInfo.hopsAway) if nodeInfo.hasDeviceMetrics { @@ -276,6 +283,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 @@ -287,8 +295,19 @@ 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) + 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) + 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) @@ -340,12 +359,18 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].snr = nodeInfo.snr fetchedNode[0].channel = Int32(nodeInfo.channel) fetchedNode[0].favorite = nodeInfo.isFavorite + fetchedNode[0].ignored = nodeInfo.isIgnored fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway) if nodeInfo.hasUser { 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 == 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) @@ -354,6 +379,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 { @@ -423,11 +455,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 { @@ -462,23 +494,25 @@ 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) { - upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), context: context) + upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, 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) } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { let moduleConfig = adminMessage.getModuleConfigResponse @@ -548,7 +582,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) @@ -578,7 +612,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) @@ -600,13 +634,16 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana } } fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) - if routingMessage.errorReason == Routing.Error.none { fetchedMessage[0].receivedACK = true } fetchedMessage[0].ackSNR = packet.rxSnr - fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + if packet.rxTime > 0 { + fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + } else { + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) + } if fetchedMessage[0].toUser != nil { fetchedMessage[0].toUser!.objectWillChange.send() @@ -639,16 +676,12 @@ 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) { - // 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 } @@ -670,7 +703,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 @@ -687,6 +720,21 @@ 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.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 + 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 @@ -695,38 +743,57 @@ 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(timeIntervalSince1970: TimeInterval(packet.rxTime)) + } else { + fetchedNode[0].lastHeard = Date() + } 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] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type") 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() + } } - // Update our live activity if there is one running, not available on mac iOS >= 16.2 + } else if telemetry.metricsType == 4 { + // Update our live activity if there is one running, not available on mac #if !targetEnvironment(macCatalyst) +#if canImport(ActivityKit) - 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), + dupeReceivedPackets: UInt32(telemetry.numRxDupe), + packetsSentRelay: UInt32(telemetry.numTxRelay), + packetsCanceledRelay: UInt32(telemetry.numTxRelayCanceled), + 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 }) @@ -737,6 +804,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage Logger.services.debug("Updated live activity.") } } +#endif #endif } } catch { @@ -757,12 +825,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 @@ -776,7 +842,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 @@ -785,17 +851,18 @@ 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) - newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) - newMessage.receivedTimestamp = Int32(Date().timeIntervalSince1970) + if packet.rxTime > 0 { + newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + } else { + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) + } newMessage.receivedACK = false newMessage.snr = packet.rxSnr newMessage.rssi = packet.rxRssi @@ -810,17 +877,47 @@ func textMessageAppPacket( 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 }) + } else { + /// Make a new to user if they are unknown + newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) } } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - if packet.rxTime > 0 { - newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + /// Set the public key for the message + if newMessage.fromUser?.pkiEncrypted ?? false && packet.pkiEncrypted { + 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 + if !packet.publicKey.isEmpty { + newMessage.fromUser?.pkiEncrypted = true + newMessage.fromUser?.publicKey = packet.publicKey + } + } + } else { + /// Make a new from user if they are unknown + newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + } + 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!) @@ -828,15 +925,12 @@ func textMessageAppPacket( 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 } @@ -855,14 +949,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)) @@ -885,7 +981,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)") @@ -893,7 +993,7 @@ func textMessageAppPacket( } } } catch { - + // Handle error } } } @@ -918,7 +1018,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/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/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 9ae100e3..3581f63f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 40.xcdatamodel + MeshtasticDataModelV 47.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..55eccab7 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 41.xcdatamodel/contents @@ -0,0 +1,448 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents new file mode 100644 index 00000000..40947b9f --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents new file mode 100644 index 00000000..ae7c08e4 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents new file mode 100644 index 00000000..687237cb --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents new file mode 100644 index 00000000..bc188564 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents @@ -0,0 +1,484 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 47.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 47.xcdatamodel/contents new file mode 100644 index 00000000..095149bd --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 47.xcdatamodel/contents @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index c1bcf91a..b7b28f35 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -3,11 +3,8 @@ import SwiftUI import CoreData import OSLog -#if canImport(TipKit) import TipKit -#endif -@available(iOS 17.0, *) @main struct MeshtasticAppleApp: App { @@ -17,8 +14,8 @@ struct MeshtasticAppleApp: App { @ObservedObject var appState: AppState - @ObservedObject - private var bleManager: BLEManager +// @ObservedObject +// private var bleManager: BLEManager private let persistenceController: PersistenceController @@ -35,10 +32,8 @@ 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 // Wire up router @@ -53,9 +48,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) } @@ -93,7 +88,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 } @@ -110,71 +105,27 @@ 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 { - 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 + .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 06d290e9..801fa955 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -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 } @@ -46,6 +43,57 @@ 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") + } + 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") + } + 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") + } + default: + break + } + if let targetValue = userInfo["target"] as? String, let deepLink = userInfo["path"] as? String, let url = URL(string: deepLink) { @@ -54,7 +102,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat } else { Logger.services.error("Failed to handle notification response: \(userInfo)") } - completionHandler() } } diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 96808c73..70e9f898 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -103,42 +103,9 @@ 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. + /// **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/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) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 3fac0312..edc21605 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 @@ -156,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 { @@ -177,6 +180,18 @@ 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) + 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 }) + newUser.hwDisplayName = dh?.displayName + } + } newNode.user = newUser if UserDefaults.newNodeNotifications { @@ -184,9 +199,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) manager.notifications = [ Notification( id: (UUID().uuidString), - title: "New Node", + title: "New Node".localized, subtitle: "\(newUser.longName ?? "unknown".localized)", - content: "New Node has been discovered", + content: "New Node has been discovered".localized, target: "nodes", path: "meshtastic:///nodes?nodenum=\(newUser.num)" ) @@ -197,6 +212,10 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { if packet.from > Constants.minimumNodeNum { let newUser = createUser(num: Int64(packet.from), context: context) + if !packet.publicKey.isEmpty { + newNode.user?.pkiEncrypted = true + newNode.user?.publicKey = packet.publicKey + } newNode.user = newUser } } @@ -210,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() @@ -224,9 +244,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 @@ -235,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 @@ -257,6 +276,17 @@ 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) + if !nodeInfoMessage.user.publicKey.isEmpty { + fetchedNode[0].user!.pkiEncrypted = true + fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey + } + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) + fetchedNode[0].user!.hwDisplayName = dh?.displayName + } + } } } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) @@ -290,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) { @@ -347,6 +377,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 @@ -364,12 +396,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 { @@ -377,7 +404,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)") @@ -394,13 +421,15 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { try context.save() @@ -419,7 +448,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)") @@ -433,30 +462,32 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, conte if fetchedNode[0].deviceConfig == nil { 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) 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?.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) 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 } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)") @@ -472,7 +503,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)") @@ -498,7 +529,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) @@ -511,7 +541,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() @@ -536,7 +569,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)") @@ -565,6 +598,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: 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) @@ -582,8 +616,13 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: 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 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -601,7 +640,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)") @@ -626,7 +665,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -645,7 +687,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)") @@ -688,6 +730,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -705,7 +751,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)") @@ -735,6 +781,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -752,7 +802,69 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context } } -func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, 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)") + + 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 + if config.adminKey.count > 0 { + newSecurityConfig.adminKey = config.adminKey[0] + } + newSecurityConfig.isManaged = config.isManaged + newSecurityConfig.serialEnabled = config.serialEnabled + newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled + newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled + fetchedNode[0].securityConfig = newSecurityConfig + } else { + fetchedNode[0].securityConfig?.publicKey = config.publicKey + 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?.adminKey2 = config.adminKey[1] + } else if config.adminKey.count > 2 { + fetchedNode[0].securityConfig?.adminKey3 = config.adminKey[2] + } + } + fetchedNode[0].securityConfig?.isManaged = config.isManaged + fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled + fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled + fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled + } + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("💾 [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + 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("💥 [SecurityConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } +} + +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)") @@ -766,16 +878,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 { @@ -787,7 +896,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -805,7 +917,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)") @@ -819,9 +931,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 @@ -832,11 +942,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 @@ -848,7 +955,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -866,7 +976,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)") @@ -878,32 +988,31 @@ 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.triggerType = Int32(config.detectionTriggerType.rawValue) 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 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?.triggerType = Int32(config.detectionTriggerType.rawValue) fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -924,7 +1033,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)") @@ -955,7 +1064,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 @@ -973,7 +1081,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -991,7 +1102,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)") @@ -1003,19 +1114,19 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") @@ -1033,7 +1144,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)") @@ -1052,6 +1163,10 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManage } else { fetchedNode[0].rtttlConfig?.ringtone = ringtone } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1069,7 +1184,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)") @@ -1081,7 +1196,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 @@ -1111,6 +1225,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") @@ -1128,7 +1246,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)") @@ -1151,6 +1269,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1168,7 +1290,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)") @@ -1180,9 +1302,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 @@ -1192,7 +1312,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 @@ -1202,7 +1321,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -1223,7 +1345,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)") @@ -1235,9 +1357,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 @@ -1245,7 +1365,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 @@ -1253,6 +1372,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1270,21 +1393,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) @@ -1295,7 +1415,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) @@ -1306,7 +1425,10 @@ 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 + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)") diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index ad538bd0..a4a6949c 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -5,7 +5,10 @@ "platformioTarget": "tlora-v2", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V2" + "displayName": "LILYGO T-LoRa V2", + "tags": [ + "LilyGo" + ] }, { "hwModel": 2, @@ -13,7 +16,10 @@ "platformioTarget": "tlora-v1", "architecture": "esp32", "activelySupported": false, - "displayName": "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": "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": "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,26 @@ "platformioTarget": "tbeam0_7", "architecture": "esp32", "activelySupported": false, - "displayName": "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": "T-Echo" + "displayName": "LILYGO T-Echo", + "tags": [ + "LilyGo" + ], + "images": [ + "t-echo.svg" + ], + "requiresDfu": true }, { "hwModel": 8, @@ -61,7 +95,10 @@ "platformioTarget": "tlora-v1_3", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V1.1-1.3" + "displayName": "LILYGO T-LoRa V1.1-1.3", + "tags": [ + "LilyGo" + ] }, { "hwModel": 9, @@ -69,7 +106,16 @@ "platformioTarget": "rak4631", "architecture": "nrf52840", "activelySupported": true, - "displayName": "RAK4631" + "supportLevel": 1, + "displayName": "RAK WisBlock 4631", + "tags": [ + "RAK" + ], + "images": [ + "rak4631.svg", + "rak4631_case.svg" + ], + "requiresDfu": true }, { "hwModel": 10, @@ -77,7 +123,10 @@ "platformioTarget": "heltec-v2_1", "architecture": "esp32", "activelySupported": false, - "displayName": "Heltec V2.1" + "displayName": "Heltec V2.1", + "tags": [ + "Heltec" + ] }, { "hwModel": 11, @@ -85,15 +134,26 @@ "platformioTarget": "heltec-v1", "architecture": "esp32", "activelySupported": false, - "displayName": "Heltec V1" + "displayName": "Heltec V1", + "tags": [ + "Heltec" + ] }, { "hwModel": 12, - "hwModelSlug": "TBEAM_S3_CORE", + "hwModelSlug": "LILYGO_TBEAM_S3_CORE", "platformioTarget": "tbeam-s3-core", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Beam S3 Core" + "supportLevel": 1, + "displayName": "LILYGO T-Beam Supreme", + "tags": [ + "LilyGo" + ], + "images": [ + "tbeam-s3-core.svg" + ], + "requiresDfu": true }, { "hwModel": 13, @@ -101,7 +161,10 @@ "platformioTarget": "rak11200", "architecture": "esp32", "activelySupported": false, - "displayName": "RAK11200" + "displayName": "RAK WisBlock 11200", + "tags": [ + "RAK" + ] }, { "hwModel": 14, @@ -109,7 +172,11 @@ "platformioTarget": "nano-g1", "architecture": "esp32", "activelySupported": true, - "displayName": "Nano G1" + "supportLevel": 3, + "displayName": "Nano G1", + "tags": [ + "B&Q" + ] }, { "hwModel": 15, @@ -117,7 +184,15 @@ "platformioTarget": "tlora-v2-1-1_8", "architecture": "esp32", "activelySupported": true, - "displayName": "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 +200,31 @@ "platformioTarget": "tlora-t3s3-v1", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-LoRa T3-S3" + "displayName": "LILYGO T-LoRa T3-S3", + "supportLevel": 1, + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-t3s3-v1.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 16, + "hwModelSlug": "TLORA_T3_S3", + "platformioTarget": "tlora-t3s3-epaper", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "LILYGO T-LoRa T3-S3 E-Ink", + "tags": [ + "LilyGo" + ], + "images": [ + "tlora-t3s3-epaper.svg" + ], + "requiresDfu": true }, { "hwModel": 17, @@ -133,7 +232,11 @@ "platformioTarget": "nano-g1-explorer", "architecture": "esp32", "activelySupported": true, - "displayName": "Nano G1 Explorer" + "supportLevel": 3, + "displayName": "Nano G1 Explorer", + "tags": [ + "B&Q" + ] }, { "hwModel": 18, @@ -141,7 +244,31 @@ "platformioTarget": "nano-g2-ultra", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Nano G2 Ultra" + "supportLevel": 2, + "displayName": "Nano G2 Ultra", + "tags": [ + "B&Q" + ], + "requiresDfu": true, + "images": [ + "nano-g2-ultra.svg" + ] + }, + { + "hwModel": 21, + "hwModelSlug": "WIO_WM1110", + "platformioTarget": "wio-tracker-wm1110", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Wio WM1110 Tracker", + "tags": [ + "Seeed" + ], + "images": [ + "wio-tracker-wm1110.svg" + ], + "requiresDfu": true }, { "hwModel": 25, @@ -149,7 +276,11 @@ "platformioTarget": "station-g1", "architecture": "esp32", "activelySupported": true, - "displayName": "Station G1" + "supportLevel": 3, + "displayName": "Station G1", + "tags": [ + "B&Q" + ] }, { "hwModel": 26, @@ -157,7 +288,15 @@ "platformioTarget": "rak11310", "architecture": "rp2040", "activelySupported": true, - "displayName": "RAK11310" + "supportLevel": 2, + "displayName": "RAK WisBlock 11310", + "tags": [ + "RAK" + ], + "images": [ + "rak11310.svg" + ], + "requiresDfu": true }, { "hwModel": 29, @@ -165,7 +304,12 @@ "platformioTarget": "canaryone", "architecture": "nrf52840", "activelySupported": true, - "displayName": "CanaryOne" + "supportLevel": 3, + "displayName": "Canary One", + "tags": [ + "Canary" + ], + "requiresDfu": true }, { "hwModel": 30, @@ -173,7 +317,12 @@ "platformioTarget": "rp2040-lora", "architecture": "rp2040", "activelySupported": true, - "displayName": "RP2040 LoRa" + "supportLevel": 2, + "displayName": "RP2040 LoRa", + "tags": [ + "Waveshare" + ], + "requiresDfu": true }, { "hwModel": 31, @@ -181,7 +330,15 @@ "platformioTarget": "station-g2", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "Station G2" + "supportLevel": 2, + "displayName": "Station G2", + "tags": [ + "B&Q" + ], + "requiresDfu": true, + "images": [ + "station-g2.svg" + ] }, { "hwModel": 39, @@ -189,7 +346,14 @@ "platformioTarget": "meshtastic-diy-v1", "architecture": "esp32", "activelySupported": true, - "displayName": "DIY V1" + "supportLevel": 3, + "displayName": "DIY V1", + "tags": [ + "DIY" + ], + "images": [ + "diy.svg" + ] }, { "hwModel": 39, @@ -197,15 +361,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, @@ -213,7 +384,11 @@ "platformioTarget": "m5stack-core", "architecture": "esp32", "activelySupported": true, - "displayName": "M5 Stack" + "supportLevel": 3, + "displayName": "M5 Stack", + "tags": [ + "M5Stack" + ] }, { "hwModel": 43, @@ -221,7 +396,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, @@ -229,7 +412,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, @@ -237,7 +427,16 @@ "platformioTarget": "pico", "architecture": "rp2040", "activelySupported": true, - "displayName": "Raspberry Pi Pico" + "supportLevel": 3, + "displayName": "Raspberry Pi Pico", + "tags": [ + "RPi", + "DIY" + ], + "requiresDfu": true, + "images": [ + "pico.svg" + ] }, { "hwModel": 47, @@ -245,7 +444,16 @@ "platformioTarget": "picow", "architecture": "rp2040", "activelySupported": true, - "displayName": "Raspberry Pi Pico W" + "supportLevel": 3, + "displayName": "Raspberry Pi Pico W", + "tags": [ + "RPi", + "DIY" + ], + "requiresDfu": true, + "images": [ + "rpipicow.svg" + ] }, { "hwModel": 48, @@ -253,15 +461,28 @@ "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" + ], + "requiresDfu": true }, { "hwModel": 58, "hwModelSlug": "HELTEC_WIRELESS_TRACKER_V1_0", "platformioTarget": "heltec-wireless-tracker-V1-0", "architecture": "esp32-s3", - "activelySupported": true, - "displayName": "Heltec Wireless Tracker V1.0" + "activelySupported": false, + "supportLevel": 3, + "displayName": "Heltec Wireless Tracker V1.0", + "images": [ + "heltec-wireless-tracker.svg" + ], + "requiresDfu": true }, { "hwModel": 49, @@ -269,7 +490,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, @@ -277,7 +505,15 @@ "platformioTarget": "t-deck", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Deck" + "supportLevel": 1, + "displayName": "LILYGO T-Deck", + "tags": [ + "LilyGo" + ], + "images": [ + "t-deck.svg" + ], + "requiresDfu": true }, { "hwModel": 51, @@ -285,7 +521,14 @@ "platformioTarget": "t-watch-s3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Watch S3" + "supportLevel": 1, + "displayName": "LILYGO T-Watch S3", + "tags": [ + "LilyGo" + ], + "images": [ + "t-watch-s3.svg" + ] }, { "hwModel": 52, @@ -293,6 +536,7 @@ "platformioTarget": "picomputer-s3", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "Pi Computer S3" }, { @@ -300,16 +544,30 @@ "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, "hwModelSlug": "HELTEC_WIRELESS_PAPER_V1_0", "platformioTarget": "heltec-wireless-paper-v1_0", "architecture": "esp32-s3", - "activelySupported": true, - "displayName": "Heltec Wireless Paper V1.0" + "activelySupported": false, + "supportLevel": 3, + "tags": [ + "Heltec" + ], + "displayName": "Heltec Wireless Paper V1.0", + "images": [ + "heltec-wireless-paper-v1_0.svg" + ] }, { "hwModel": 59, @@ -317,7 +575,9 @@ "platformioTarget": "unphone", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "unPhone" + "supportLevel": 3, + "displayName": "unPhone", + "requiresDfu": true }, { "hwModel": 48, @@ -325,7 +585,9 @@ "platformioTarget": "tracksenger", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "TrackSenger (small TFT)" + "supportLevel": 3, + "displayName": "TrackSenger (small TFT)", + "requiresDfu": true }, { "hwModel": 48, @@ -333,7 +595,9 @@ "platformioTarget": "tracksenger-lcd", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "TrackSenger (big TFT)" + "supportLevel": 3, + "displayName": "TrackSenger (big TFT)", + "requiresDfu": true }, { "hwModel": 48, @@ -341,6 +605,7 @@ "platformioTarget": "tracksenger-oled", "architecture": "esp32-s3", "activelySupported": true, + "supportLevel": 3, "displayName": "TrackSenger (big OLED)" }, { @@ -349,7 +614,12 @@ "platformioTarget": "CDEBYTE_EoRa-S3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "EBYTE EoRa-S3" + "supportLevel": 3, + "displayName": "EBYTE EoRa-S3", + "tags": [ + "EByte" + ], + "requiresDfu": true }, { "hwModel": 64, @@ -357,14 +627,138 @@ "platformioTarget": "radiomaster_900_bandit_nano", "architecture": "esp32", "activelySupported": true, - "displayName": "RadioMaster 900 Bandit Nano" + "supportLevel": 2, + "displayName": "RadioMaster 900 Bandit Nano", + "tags": [ + "RadioMaster" + ] }, { - "hwModel": 21, - "hwModelSlug": "WIO_WM1110", - "platformioTarget": "wio-tracker-wm1110", + "hwModel": 66, + "hwModelSlug": "HELTEC_VISION_MASTER_T190", + "platformioTarget": "heltec-vision-master-t190", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Vision Master T190", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-t190.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 67, + "hwModelSlug": "HELTEC_VISION_MASTER_E213", + "platformioTarget": "heltec-vision-master-e213", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Vision Master E213", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-e213.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 68, + "hwModelSlug": "HELTEC_VISION_MASTER_E290", + "platformioTarget": "heltec-vision-master-e290", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec Vision Master E290", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-vision-master-e290.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 69, + "hwModelSlug": "HELTEC_MESH_NODE_T114", + "platformioTarget": "heltec-mesh-node-t114", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Seeed Wio WM1110 Tracker" + "supportLevel": 1, + "displayName": "Heltec Mesh Node T114", + "tags": [ + "Heltec" + ], + "images": [ + "heltec-mesh-node-t114.svg", + "heltec-mesh-node-t114-case.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 70, + "hwModelSlug": "SENSECAP_INDICATOR", + "platformioTarget": "seeed-sensecap-indicator", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed SenseCAP Indicator", + "tags": [ + "Seeed" + ], + "images": [ + "seeed-sensecap-indicator.svg" + ] + }, + { + "hwModel": 71, + "hwModelSlug": "TRACKER_T1000_E", + "platformioTarget": "tracker-t1000-e", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Card Tracker T1000-E", + "tags": [ + "Seeed" + ], + "images": [ + "tracker-t1000-e.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 72, + "hwModelSlug": "Seeed_XIAO_S3", + "platformioTarget": "seeed-xiao-s3", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Xiao ESP32-S3", + "tags": [ + "Seeed" + ], + "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 } ] diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index b33fc48a..0a85f4ab 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 @@ -54,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 @@ -73,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/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index f7540b66..02d2af53 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -5,11 +5,8 @@ // Copyright(c) Garth Vander Houwen 8/31/23. // import SwiftUI -#if canImport(TipKit) import TipKit -#endif -@available(iOS 17.0, macOS 14.0, *) struct BluetoothConnectionTip: Tip { var id: String { diff --git a/Meshtastic/Tips/ChannelTips.swift b/Meshtastic/Tips/ChannelTips.swift index be241951..712a266e 100644 --- a/Meshtastic/Tips/ChannelTips.swift +++ b/Meshtastic/Tips/ChannelTips.swift @@ -5,11 +5,8 @@ // Copyright(c) Garth Vander Houwen 8/31/23. // import SwiftUI - #if canImport(TipKit) import TipKit - #endif - @available(iOS 17.0, macOS 14.0, *) struct ShareChannelsTip: Tip { var id: String { @@ -26,7 +23,6 @@ } } -@available(iOS 17.0, macOS 14.0, *) struct CreateChannelsTip: Tip { var id: String { @@ -43,7 +39,6 @@ struct CreateChannelsTip: Tip { } } -@available(iOS 17.0, macOS 14.0, *) struct AdminChannelTip: Tip { var id: String { diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index d78daa0e..ddbe9feb 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -5,11 +5,8 @@ // Copyright(c) Garth Vander Houwen 9/15/23. // import SwiftUI -#if canImport(TipKit) import TipKit -#endif -@available(iOS 17.0, macOS 14.0, *) struct MessagesTip: Tip { var id: String { @@ -25,22 +22,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("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.") - } - var image: Image? { - Image(systemName: "person.circle") - } -} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 0048e430..a424cbf0 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -11,9 +11,7 @@ import CoreData import CoreLocation import CoreBluetooth import OSLog -#if canImport(TipKit) import TipKit -#endif #if canImport(ActivityKit) import ActivityKit #endif @@ -49,45 +47,47 @@ 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) - } - 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) + TipView(BluetoothConnectionTip(), arrowEdge: .bottom) + 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, *) { + 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 { 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) } } } } .font(.caption) .foregroundColor(Color.gray) - .padding([.top, .bottom]) + .padding([.top]) .swipeActions { Button(role: .destructive) { if let connectedPeripheral = bleManager.connectedPeripheral, @@ -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 { @@ -324,20 +324,32 @@ struct Connect: View { } } } - #if canImport(ActivityKit) +#if !targetEnvironment(macCatalyst) +#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 == 4")) + 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), + 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) - 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, @@ -356,8 +368,8 @@ struct Connect: View { } } } - #endif - +#endif +#endif func didDismissSheet() { bleManager.disconnectPeripheral(reconnect: false) } diff --git a/Meshtastic/Views/Bluetooth/InvalidVersion.swift b/Meshtastic/Views/Bluetooth/InvalidVersion.swift index 9c0cca78..ba94e7c1 100644 --- a/Meshtastic/Views/Bluetooth/InvalidVersion.swift +++ b/Meshtastic/Views/Bluetooth/InvalidVersion.swift @@ -41,11 +41,9 @@ struct InvalidVersion: View { .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) includes substantial network optimizations and extensive changes to devices and 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 { diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index b109a318..b122b0aa 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -4,23 +4,15 @@ import SwiftUI -@available(iOS 17.0, *) struct ContentView: View { @ObservedObject var appState: AppState - + @ObservedObject 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, @@ -46,19 +38,11 @@ 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/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/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..fd0f0414 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift @@ -0,0 +1,76 @@ +// +// 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 { + Label("Direct Message Help", systemImage: "questionmark.circle") + .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() + .padding(.trailing) + } + } +#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..f8dacd2f --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -0,0 +1,66 @@ +// +// LockLegend.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 8/15/24. +// + +import SwiftUI + +struct LockLegend: View { + + var body: some View { + VStack(alignment: .leading) { + Text("What does the lock mean?") + .font(.title2) + .padding(.bottom, 5) + 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.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) + .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 for encryption. Requires firmware version 2.5 or greater.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) + .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 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) + } + } +} + +struct LockLegendPreviews: PreviewProvider { + static var previews: some View { + VStack { + LockLegend() + } + } +} 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) } } } diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift index 4405e819..688dcc24 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift @@ -47,13 +47,13 @@ enum LoRaSignalStrength: Int { var description: String { switch self { case .none: - return "None" + return "lora.signal.strength.none".localized case .bad: - return "Bad" + return "lora.signal.strength.bad".localized case .fair: - return "Fair" + return "lora.signal.strength.fair".localized case .good: - return "Good" + return "lora.signal.strength.good".localized } } } @@ -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) { diff --git a/Meshtastic/Views/Helpers/MQTTIcon.swift b/Meshtastic/Views/Helpers/MQTTIcon.swift index 79821dd1..914c6043 100644 --- a/Meshtastic/Views/Helpers/MQTTIcon.swift +++ b/Meshtastic/Views/Helpers/MQTTIcon.swift @@ -27,7 +27,7 @@ struct MQTTIcon: View { .symbolRenderingMode(.hierarchical) }.popover(isPresented: self.$isPopoverOpen, arrowEdge: .bottom, content: { VStack(spacing: 0.5) { - Text("Topic: " + topic) + Text("Topic: \(topic)".localized) .padding(20) Button("close", action: { self.isPopoverOpen = false }).padding([.bottom], 20) } diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift new file mode 100644 index 00000000..6bcab1d0 --- /dev/null +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -0,0 +1,62 @@ +// +// 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 + @Binding private var isValid: Bool + @State var isSecure: Bool = true + private var title: String + + init(_ title: String, text: Binding, isValid: Binding) { + self.title = title + self._text = text + self._isValid = isValid + } + + 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) + } 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) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(isValid ? Color.clear : Color.red, lineWidth: 2.0) + ) + } + }.padding(.trailing, 36) + + if !text.isEmpty { + Button(action: { + isSecure.toggle() + }) { + Image(systemName: self.isSecure ? "eye.slash" : "eye") + .accentColor(.secondary) + } + } + } + } +} diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 8f057610..e4867544 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -95,13 +95,21 @@ 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: 90) : .system(size: 60) ) + .font(temperature.length < 4 ? .system(size: 72) : .system(size: 54) ) } - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -111,19 +119,24 @@ 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) } - .padding(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -134,17 +147,21 @@ struct PressureCompactWidget: View { let low: Bool var body: some View { VStack(alignment: .leading) { - Label { Text("PRESSURE") } icon: { Image(systemName: "gauge").symbolRenderingMode(.multicolor) } - .font(.caption2) + 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) } - .padding(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -156,17 +173,17 @@ 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) + .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(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) + .padding() .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift new file mode 100644 index 00000000..fd4d7d92 --- /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/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)! -// } -// } -// } 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 5c7291a5..8cdf6d84 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -1,463 +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 - 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) - } - 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) - } - } - } - 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 - self.configureMap(mapView: mapView) - 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 - 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/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/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index d5fd9c01..456472e0 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -1,261 +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, perform: { _ in - let totalBytes = name.utf8.count - // Only mess with the value if it is too big - if totalBytes > 30 { - name = String(name.dropLast()) - } - }) - } - HStack { - Text("Description") - Spacer() - TextField( - "Description", - text: $description, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: description, perform: { _ in - let totalBytes = description.utf8.count - // Only mess with the value if it is too big - if totalBytes > 100 { - description = String(description.dropLast()) - } - }) - } - 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/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index b996e790..1b783427 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -84,19 +84,20 @@ struct ChannelMessageList: View { } HStack { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) if currentUser && message.receivedACK { - // Ack Received - Text("Acknowledged").font(.caption2).foregroundColor(.gray) + Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) + .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 currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("Waiting to be acknowledged. . .").font( + .caption2) + .foregroundColor(.orange) + } else if currentUser && !isDetectionSensorMessage { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) - } else if isDetectionSensorMessage { - let messageDate = message.timestamp - Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) + .foregroundStyle(ackErrorVal?.color ?? Color.red) + .font(.caption2) } } } @@ -128,16 +129,16 @@ 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) + .onChange(of: channel.allPrivateMessages) { + withAnimation { + scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) } - }) + } } TextMessageField( diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index ab363f59..c9c37cdc 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/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 0cdd4be8..df5b1f3d 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -24,31 +24,37 @@ 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 { + /// 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 { + 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 { - 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 88a6f2a2..1267d3e2 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -8,9 +8,7 @@ import SwiftUI import CoreData import OSLog -#if canImport(TipKit) import TipKit -#endif struct Messages: View { @@ -26,21 +24,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 +32,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") @@ -80,19 +63,18 @@ struct Messages: View { } } - if #available(iOS 17.0, macOS 14.0, *) { - TipView(MessagesTip(), arrowEdge: .top) - } + TipView(MessagesTip(), arrowEdge: .top) } .navigationTitle("messages") .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,12 +82,12 @@ 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 + }.onChange(of: router.navigationState) { setupNavigationState() } } @@ -116,11 +98,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/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 { diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index 469e57a4..1eca5015 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 @@ -30,13 +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 - if totalBytes > Self.maxbytes { + while totalBytes > Self.maxbytes { typingMessage = String(typingMessage.dropLast()) + totalBytes = typingMessage.utf8.count } - }) + } .keyboardType(.default) .toolbar { ToolbarItemGroup(placement: .keyboard) { 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) } } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 226d2ae9..a8986250 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -8,9 +8,7 @@ import SwiftUI import CoreData import OSLog -#if canImport(TipKit) import TipKit -#endif struct UserList: View { @@ -20,24 +18,38 @@ 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 isIgnored = 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 = [] - @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, + isEnvironment, + distanceFilter, + roleFilter + ]} @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "userNode.favorite", ascending: false), + NSSortDescriptor(key: "pkiEncrypted", ascending: false), + NSSortDescriptor(key: "userNode.lastHeard", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], predicate: NSPredicate( - format: "NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)" + format: "userNode.ignored == false && longName != '' AND NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)" ), animation: .default ) - private var users: FetchedResults + var users: FetchedResults @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? @@ -49,9 +61,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 )))) @@ -71,8 +80,22 @@ struct UserList: View { VStack(alignment: .leading) { HStack { + if user.pkiEncrypted { + if !user.keyMatch { + /// Public Key on the User and the Public Key on the Last Message don't match + Image(systemName: "key.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } Text(user.longName ?? "unknown".localized) .font(.headline) + .allowsTightening(true) Spacer() if user.userNode?.favorite ?? false { Image(systemName: "star.fill") @@ -171,75 +194,104 @@ 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) + .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count))) + .sheet(isPresented: $editingFilters) { + NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } - .onChange(of: searchText) { _ in - searchUserList() + .sheet(isPresented: $showingHelp) { + DirectMessagesHelp() } - .onChange(of: viaLora) { _ in + .onChange(of: searchText) { + Task { + await searchUserList() + } + } + .onChange(of: viaLora) { if !viaLora && !viaMqtt { viaMqtt = true } - searchUserList() + Task { + await searchUserList() + } } - .onChange(of: viaMqtt) { _ in + .onChange(of: viaMqtt) { if !viaLora && !viaMqtt { viaLora = true } - searchUserList() + Task { + await searchUserList() + } } - .onChange(of: [deviceRoles]) { _ in - searchUserList() + .onChange(of: [deviceRoles]) { + Task { + await searchUserList() + } } - .onChange(of: hopsAway) { _ in - searchUserList() + .onChange(of: hopsAway) { + Task { + await searchUserList() + } } - .onChange(of: isOnline) { _ in - searchUserList() + .onChange(of: [boolFilters]) { + Task { + await searchUserList() + } } - .onChange(of: isFavorite) { _ in - searchUserList() + .onChange(of: maxDistance) { + Task { + await searchUserList() + } } - .onChange(of: maxDistance) { _ in - searchUserList() - } - .onChange(of: distanceFilter) { _ in - searchUserList() + .onChange(of: isPkiEncrypted) { + Task { + await searchUserList() + } } .onAppear { - searchUserList() + Task { + await 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) } } - - private func searchUserList() { + private func searchUserList() async { /// 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 @@ -279,9 +331,14 @@ 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 + 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/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6514ead8..7ce190fc 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 @@ -72,7 +72,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) } @@ -81,7 +83,8 @@ 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) + .font(.caption2) } } } @@ -114,16 +117,16 @@ 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) + .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 51a68a49..0239e476 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 { @@ -47,7 +48,6 @@ struct DeviceMetricsLog: View { .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") .foregroundStyle(batteryChartColor) .interpolationMethod(.linear) - Plot { PointMark( x: .value("x", point.time!), @@ -58,14 +58,29 @@ struct DeviceMetricsLog: View { .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) +// ) { +// 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!), @@ -82,6 +97,7 @@ struct DeviceMetricsLog: View { AxisMarks(position: .top) }) .chartXAxis(.automatic) + .chartXSelection(value: $chartSelection) .chartYScale(domain: 0...100) .chartForegroundStyleScale([ idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, @@ -191,12 +207,14 @@ struct DeviceMetricsLog: View { .padding(.bottom) .padding(.trailing) } - } else { - if #available (iOS 17, *) { - ContentUnavailableView("No Device Metrics", systemImage: "slash.circle") - } else { - Text("No Device Metrics") + .onChange(of: selection) { _, newSelection in + guard let metrics = deviceMetrics.first(where: { $0.id == newSelection }) else { + return + } + chartSelection = metrics.time } + } else { + 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 f0d2dd04..56b0c82c 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -93,6 +93,14 @@ struct EnvironmentMetricsLog: View { IndoorAirQuality(iaq: Int(em.iaq), displayMode: IaqDisplayMode.dot ) } } + TableColumn("Wind Speed") { em in + 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 + let direction = cardinalValue(from: Double(em.windDirection)) + Text(direction) + } TableColumn("timestamp") { em in Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) } @@ -185,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/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index 247a4380..ecf16f13 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -3,49 +3,58 @@ 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 - @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/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index ed5fce47..f98bc999 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") @@ -29,7 +41,14 @@ 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 + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Failed to get a valid position to exchange.") } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift new file mode 100644 index 00000000..0ec05164 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -0,0 +1,45 @@ +import CoreData +import OSLog +import SwiftUI + +struct IgnoreNodeButton: View { + var bleManager: BLEManager + var context: NSManagedObjectContext + + @ObservedObject + var node: NodeInfoEntity + + var body: some View { + Button { + guard let connectedNodeNum = bleManager.connectedPeripheral?.num else { return } + let success = if node.ignored { + bleManager.removeIgnoredNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } else { + bleManager.setIgnoredNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } + if success { + node.ignored = !node.ignored + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Ignored Node Error") + } + Logger.data.debug("Ignored a node") + } + } label: { + Label { + Text(node.ignored ? "Remove from ignored" : "Ignore Node") + } icon: { + Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") + .symbolRenderingMode(.multicolor) + } + } + } +} 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/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index a0c2f63b..c9fbf95a 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -8,7 +8,6 @@ import SwiftUI import MapKit import CoreData -@available(iOS 17.0, macOS 14.0, *) struct NodeMapContent: MapContent { @ObservedObject var node: NodeInfoEntity diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 38b96a5d..088787b9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -6,11 +6,8 @@ // import SwiftUI -#if canImport(MapKit) import MapKit -#endif -@available(iOS 17.0, macOS 14.0, *) struct MapSettingsForm: View { @Environment(\.dismiss) private var dismiss @State private var currentDetent = PresentationDetent.medium @@ -39,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 { @@ -53,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 7906f8b5..36d9a915 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -7,11 +7,8 @@ import SwiftUI import CoreLocation -#if canImport(MapKit) import MapKit -#endif -@available(iOS 17.0, macOS 14.0, *) struct NodeMapSwiftUI: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -27,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 = 10000.0 @State var scene: MKLookAroundScene? @State var isLookingAround = false @State var isShowingAltitude = false @@ -47,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) @@ -83,7 +81,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 @@ -106,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 { @@ -130,7 +128,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 mrCoord = mostRecent?.coordinate { + position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0)) + } } if self.scene == nil { Task { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift index 2bbeaea1..5bde5a14 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift @@ -7,16 +7,13 @@ import SwiftUI import Charts -#if canImport(MapKit) import MapKit -#endif struct PositionAltitude { let time: Date var altitude: Measurement } -@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 faa49760..fb019e0b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -8,11 +8,12 @@ import SwiftUI 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 + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.dismiss) private var dismiss var position: PositionEntity var popover: Bool = true @@ -51,9 +52,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) @@ -66,16 +71,42 @@ 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 { - Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))") - .foregroundColor(.primary) + 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) + .font(idiom == .phone ? .callout : .body) + } else { + Text(altitudeFormatter.string(from: distanceInFeet)) + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } + } icon: { Image(systemName: "mountain.2.fill") .symbolRenderingMode(.hierarchical) @@ -88,6 +119,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) @@ -100,6 +132,7 @@ struct PositionPopover: View { Label { Text("Sequence: \(String(position.seqNo))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "number") .symbolRenderingMode(.hierarchical) @@ -119,11 +152,28 @@ 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 { 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) @@ -134,6 +184,7 @@ struct PositionPopover: View { Label { Text("MQTT") + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "network") .symbolRenderingMode(.hierarchical) @@ -142,20 +193,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) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - } - } Spacer() } Spacer() @@ -176,24 +213,18 @@ 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!) } - 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() } } @@ -213,9 +244,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)) } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 17573745..71d62590 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 { @@ -60,13 +65,14 @@ struct WaypointForm: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: name, perform: { _ in - let totalBytes = name.utf8.count + .onChange(of: name) { + 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 } - }) + } } HStack { Text("Description") @@ -77,13 +83,14 @@ struct WaypointForm: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: description, perform: { _ in - let totalBytes = description.utf8.count + .onChange(of: description) { + 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 } - }) + } } HStack { Text("Icon") @@ -91,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() { @@ -124,6 +131,7 @@ struct WaypointForm: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } + .scrollDismissesKeyboard(.immediately) HStack { Button { /// Send a new or exiting waypoint @@ -239,7 +247,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 +258,7 @@ struct WaypointForm: View { } else { Button { editMode = true + selectedDetent = .fraction(0.85) } label: { Image(systemName: "square.and.pencil" ) .font(.largeTitle) @@ -269,22 +278,30 @@ 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 { - 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.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 +309,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 +319,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 { @@ -378,7 +393,8 @@ struct WaypointForm: View { longitude = waypoint.coordinate.longitude } } - .presentationDetents([.fraction(0.75)]) + .presentationDetents(detents, selection: $selectedDetent) + .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index fc9b44c1..d9fc50a4 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -16,6 +16,9 @@ struct NodeDetail: View { formatter.unitsStyle = .full return formatter }() + var modemPreset: ModemPresets = ModemPresets( + rawValue: UserDefaults.modemPreset + ) ?? ModemPresets.longFast @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -43,8 +46,53 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { + HStack(alignment: .center) { + Spacer() + CircleText( + text: node.user?.shortName ?? "?", + color: Color(UIColor(hex: UInt32(node.num))), + circleSize: 75 + ) + if node.snr != 0 && !node.viaMqtt && node.hopsAway == 0 { + 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(.caption) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption) + } + } + if node.telemetries?.count ?? 0 > 0 { + Spacer() + BatteryGauge(node: node) + } + Spacer() + } + .listRowSeparator(.hidden) + if let user = node.user { + if !user.keyMatch { + Label { + VStack(alignment: .leading) { + Text("Public Key Mismatch") + .font(.title3) + .foregroundStyle(.red) + 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") + .symbolRenderingMode(.multicolor) + .foregroundStyle(.red) + } + } + } HStack { Label { Text("Node Number") @@ -65,7 +113,7 @@ struct NodeDetail: View { .symbolRenderingMode(.multicolor) } Spacer() - Text(node.user?.userId ?? "?") + Text(node.num.toHex()) .textSelection(.enabled) } @@ -177,7 +225,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) @@ -200,11 +252,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") @@ -239,19 +287,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) @@ -281,12 +327,6 @@ struct NodeDetail: View { } Section("Actions") { - FavoriteNodeButton( - bleManager: bleManager, - context: context, - node: node - ) - if let user = node.user { NodeAlertsButton( context: context, @@ -295,19 +335,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 +357,6 @@ struct NodeDetail: View { node: node ) } - DeleteNodeButton( bleManager: bleManager, context: context, @@ -399,3 +440,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 "" + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index e1626b6b..9f3b8105 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -11,66 +11,74 @@ import MapKit struct NodeInfoItem: View { - @ObservedObject - var node: NodeInfoEntity - - var modemPreset: ModemPresets = ModemPresets( - rawValue: UserDefaults.modemPreset - ) ?? ModemPresets.longFast + @ObservedObject var node: NodeInfoEntity + @State private var currentDevice: DeviceHardware? 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 let user = node.user { + ViewThatFits(in: .horizontal) { + HStack { + Spacer() if user.hwModel != "UNSET" { - Image(user.hardwareImage ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 75, height: 75) - .cornerRadius(5) - Text(String(node.user!.hwModel ?? "unset".localized)) - .font(.caption2) - .frame(maxWidth: 100) - } 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) + VStack(alignment: .center) { + Spacer() + Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 75, height: 75) + .foregroundStyle(currentDevice?.activelySupported ?? false ? .green : .red) + Text( currentDevice?.activelySupported ?? false ? "Supported" : "Unsupported") + .foregroundStyle(.gray) + .font(.callout) + } + Spacer() + } + VStack(alignment: .center) { + HStack { + if user.hardwareImage != "UNSET" { + Image(user.hardwareImage ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 150) + .cornerRadius(5) + } else { + Image(systemName: "person.crop.circle.badge.questionmark") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 75, height: 75) + .cornerRadius(5) + } + } + } + Spacer() + } + .onAppear { + Api().loadDeviceHardwareData { (hw) in + for device in hw { + let currentHardware = node.user?.hwModel ?? "UNSET" + let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "").uppercased() + if deviceString == currentHardware { + currentDevice = device + } + } } } } - - 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(.caption2) + .listRowSeparator(.hidden) + HStack { + Label { + Text("Model") + } icon: { + Image(systemName: "flipphone") + .symbolRenderingMode(.hierarchical) + } + Spacer() + if user.hwModel != "UNSET" { + Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "unset".localized))) + } else { + Text(String("incomplete".localized)) } - .frame(minWidth: 100, maxWidth: 140) } - - if node.telemetries?.count ?? 0 > 0 { - BatteryGauge(node: node) - .padding() - } - Spacer() } - .padding(.leading) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 6236f7b7..454b3607 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -15,7 +15,9 @@ 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 isIgnored: Bool @Binding var isEnvironment: Bool @Binding var distanceFilter: Bool @Binding var maximumDistance: Double @@ -64,6 +66,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 { @@ -76,19 +91,31 @@ struct NodeListFilter: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) - - Toggle(isOn: $isEnvironment) { + Toggle(isOn: $isIgnored) { Label { - Text("Environment") + Text("Ignored") } icon: { - Image(systemName: "cloud.sun") + + Image(systemName: "minus.circle.fill") .symbolRenderingMode(.multicolor) } } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) + if filterTitle == "Node Filters" { + Toggle(isOn: $isEnvironment) { + Label { + Text("Environment") + } icon: { + Image(systemName: "cloud.sun") + .symbolRenderingMode(.multicolor) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + } Toggle(isOn: $distanceFilter) { Label { @@ -173,9 +200,9 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.medium, .large]) + .presentationDetents([.fraction(0.75), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) - .presentationBackgroundInteraction(.enabled(upThrough: .medium)) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a68a63cd..b75d5822 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -21,17 +21,32 @@ 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) - .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 { + 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) - .fontWeight(.medium) .font(.headline) + .fontWeight(.regular) + .allowsTightening(true) if node.favorite { Spacer() Image(systemName: "star.fill") @@ -86,25 +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) - } - } - } 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") @@ -113,7 +112,18 @@ 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") + .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) } } } @@ -124,7 +134,6 @@ struct NodeListItem: View { HStack { Image(systemName: "\(node.channel).circle.fill") .font(.title2) - .symbolRenderingMode(.multicolor) .frame(width: 30) Text("Channel") .foregroundColor(.secondary) @@ -147,41 +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.fill") + 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 #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) } } } @@ -195,7 +199,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 { diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 71cbfaf2..65928ba3 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 @@ -33,22 +30,48 @@ 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 distance = 10000.0 + @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 isIgnored = 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 { 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) @@ -61,6 +84,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() }) @@ -79,6 +105,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) @@ -106,14 +133,14 @@ 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) { - guard case .map(let selectedNodeNum) = router.navigationState else { return } - //TODO: handle deep link for waypoints + 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 @@ -128,14 +155,31 @@ struct MeshMap: View { return } } + .sheet(isPresented: $editingFilters) { + NodeListFilter( + viaLora: $viaLora, + viaMqtt: $viaMqtt, + isOnline: $isOnline, + isPkiEncrypted: $isPkiEncrypted, + isFavorite: $isFavorite, + isIgnored: $isIgnored, + isEnvironment: $isEnvironment, + distanceFilter: $distanceFilter, + maximumDistance: $maxDistance, + hopsAway: $hopsAway, + roleFilter: $roleFilter, + deviceRoles: $deviceRoles + ) + } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { + Spacer() 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)) @@ -149,7 +193,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) @@ -170,4 +214,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/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 4d7ec453..6f031018 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -24,17 +24,26 @@ 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 isIgnored = 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 = [] + @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, isFavorite, + isIgnored, + isOnline, + isPkiEncrypted, isEnvironment, distanceFilter, roleFilter @@ -46,6 +55,7 @@ struct NodeList: View { @FetchRequest( sortDescriptors: [ + NSSortDescriptor(key: "ignored", ascending: true), NSSortDescriptor(key: "favorite", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false), NSSortDescriptor(key: "user.longName", ascending: true) @@ -66,12 +76,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 +84,69 @@ struct NodeList: View { user: user ) } - if let connectedNode { - ExchangePositionsButton( - bleManager: bleManager, - node: node - ) - TraceRouteButton( - bleManager: bleManager, - node: node - ) - DeleteNodeButton( + /// Favoriting a node requires being connected + FavoriteNodeButton( bleManager: bleManager, context: context, - connectedNode: connectedNode, node: 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, + 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") + } + IgnoreNodeButton( + bleManager: bleManager, + context: context, + node: node + ) + Button(role: .destructive) { + deleteNodeId = node.num + isPresentingDeleteNodeAlert = true + } label: { + Label("Delete Node", systemImage: "trash") + } + } } } @@ -118,7 +170,9 @@ struct NodeList: View { viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, + isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, + isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, @@ -150,6 +204,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(), @@ -191,26 +283,18 @@ 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 + .onChange(of: searchText) { Task { await searchNodeList() } } - .onChange(of: viaLora) { _ in + .onChange(of: viaLora) { if !viaLora && !viaMqtt { viaMqtt = true } @@ -218,7 +302,7 @@ struct NodeList: View { await searchNodeList() } } - .onChange(of: viaMqtt) { _ in + .onChange(of: viaMqtt) { if !viaLora && !viaMqtt { viaLora = true } @@ -226,37 +310,34 @@ 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 - // Handle deep link routing - if case .nodes(let selected) = router.navigationState { - self.selectedNode = selected.flatMap { - getNodeInfo(id: $0, context: context) - } + .onChange(of: router.navigationState) { + if let selected = router.navigationState.nodeListSelectedNodeNum { + self.selectedNode = getNodeInfo(id: selected, context: context) } else { self.selectedNode = nil } @@ -270,7 +351,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 @@ -307,14 +388,27 @@ 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 + if isPkiEncrypted { + let isPkiEncryptedPredicate = NSPredicate(format: "user.pkiEncrypted == YES") + predicates.append(isPkiEncryptedPredicate) + } /// Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "favorite == YES") predicates.append(isFavoritePredicate) } + /// Ignored + if isIgnored { + let isIgnoredPredicate = NSPredicate(format: "ignored == YES") + predicates.append(isIgnoredPredicate) + } else if !isIgnored { + let isIgnoredPredicate = NSPredicate(format: "ignored == NO") + predicates.append(isIgnoredPredicate) + } /// Environment if isEnvironment { let environmentPredicate = NSPredicate(format: "SUBQUERY(telemetries, $tel, $tel.metricsType == 1).@count > 0") diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 5630fa8f..e81fc224 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -1,251 +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 enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles - @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 +// @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: "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 - @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) { - 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 - } - } - Divider() - Toggle(isOn: $enableOfflineMapsMBTiles) { - Text("Enable MB Tiles") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.enableOfflineMapsMBTiles.toggle() - UserDefaults.enableOfflineMapsMBTiles = self.enableOfflineMapsMBTiles - } - Text("The latest MBTiles file shared with meshtastic will be loaded into the map.") - .font(.footnote) - .foregroundColor(.gray) - } - } - } - } - #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 - }) - } -} +// 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 +// }) +// } +//} 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/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 9556298a..dd4223f7 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -6,16 +6,15 @@ // import SwiftUI -#if canImport(MapKit) +import CoreData +import OSLog import MapKit -#endif -@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 - @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" @@ -26,118 +25,202 @@ 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() + /// 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 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")") + let routeTime = route.time?.formatted() ?? "unknown".localized + if route.response && route.hopsTowards == route.hopsBack { + let hopString = String(localized: "\(route.hopsTowards) Hops") + Text("\(routeTime) - \(hopString)") + .font(.caption) + } else if route.response { + let hopTowardsString = String(localized: "\(route.hopsTowards) Hops") + let hopBackString = String(localized: "\(route.hopsBack) Hops") + Text("\(routeTime) - \(hopTowardsString) Towards \(hopBackString) Back") + .font(.caption) + } else if route.sent { + Text("\(routeTime) - No Response") + .font(.caption) + } else { + Text("\(routeTime) - 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) } + .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) } - .frame(minHeight: 200, maxHeight: 230) - VStack { + Divider() + ScrollView { if selectedRoute != nil { - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { - + 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?.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: { - Image(systemName: "signpost.right.and.left") + Image(systemName: "signpost.right") .symbolRenderingMode(.hierarchical) } - .font(.title2) - } else if selectedRoute?.response ?? false { + .font(.title3) Label { - Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Route Back: \(selectedRoute?.routeBackText ?? "unknown".localized)") } icon: { - Image(systemName: "signpost.right.and.left") + Image(systemName: "signpost.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) - } - } else if selectedRoute?.hops?.count ?? 0 == 0 { - - } - } - .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) - } - } - } - } - } else { - VStack { + .font(.title3) + } else if !(selectedRoute?.sent ?? true) { Label { - Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + 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: "signpost.right.and.left") + Image(systemName: "square.and.arrow.up.trianglebadge.exclamationmark") .symbolRenderingMode(.hierarchical) } - .font(.title3) - Spacer() + } else { + Label { + VStack { + Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + .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() + } + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + } + 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) + Spacer() + TraceRoute(radius: size < 600 ? size : 600, rotation: angle) { + contents() + } + .padding(.leading, idiom == .phone ? 0 : 20) + Spacer() + } + .scaledToFit() } + .onAppear { + // 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)) + } + } + } + .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) +// } +// } +// } +// } +// } + Spacer() + .padding(.bottom, 125) } } else { ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } } - Spacer() + .edgesIgnoringSafeArea(.bottom) } .navigationTitle("Trace Route Log") } @@ -146,4 +229,69 @@ 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.. [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 } diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index bed0a948..3f1ebf0e 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: { @@ -69,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 2ed2b081..b96ed806 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.4, macOS 14.4, *) struct AppLog: View { @State private var logs: [OSLogEntryLog] = [] @@ -33,85 +31,125 @@ struct AppLog: View { .secondFraction(.fractional(3)) var body: some View { + HStack { - 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 + if idiom == .phone { + Table(logs, selection: $selection, sortOrder: $sortOrder) { + TableColumn("log.message", value: \.composedMessage) { value in + Text(value.composedMessage) + .foregroundStyle(value.level.color) + .font(.caption) } - }) { - 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("Loading Logs. . .", 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(.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("Loading Logs. . .", 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 { 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 @@ -176,7 +214,6 @@ struct AppLog: View { } } -@available(iOS 17.4, macOS 14.4, *) extension AppLog { @MainActor private func searchAppLogs() async -> [OSLogEntryLog] { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index f9f46cbc..f641b77b 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,13 @@ struct AppSettings: View { UIApplication.shared.open(url) } } + Toggle(isOn: $enableAdministration) { + 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) { @@ -68,9 +76,14 @@ struct AppSettings: View { } clearCoreDataDatabase(context: context, includeRoutes: true) context.refreshAllObjects() - UserDefaults.standard.reset() } } + Button { + UserDefaults.standard.reset() + } label: { + Label("Reset App Settings", systemImage: "arrow.counterclockwise.circle") + .foregroundColor(.red) + } } if totalDownloadedTileSize != "0MB" { Section(header: Text("Map Tile Data")) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index bec86958..1a28a6be 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) @@ -62,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: { @@ -92,13 +88,21 @@ struct Channels: View { positionPrecision = 32 preciseLocation = true positionsEnabled = true - + if channelKey == "AQ==" { + positionPrecision = 14 + preciseLocation = false + } } else if !supportedVersion && channelRole == 2 { positionPrecision = 0 preciseLocation = false positionsEnabled = false } else { - if positionPrecision == 32 { + if channelKey == "AQ==" { + preciseLocation = false + if (positionPrecision > 0 && positionPrecision < 11) || positionPrecision > 14 { + positionPrecision = 14 + } + } else if positionPrecision == 32 { preciseLocation = true positionsEnabled = true } else { @@ -146,7 +150,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 { @@ -217,7 +221,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) @@ -253,7 +257,6 @@ struct Channels: View { positionPrecision = 0 uplink = false downlink = false - hasChanges = true let newChannel = ChannelEntity(context: context) newChannel.id = channelIndex @@ -265,6 +268,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 780a19b4..06a4a260 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 { @@ -41,15 +39,16 @@ struct ChannelForm: View { .disableAutocorrection(true) .keyboardType(.alphabet) .foregroundColor(Color.gray) - .onChange(of: channelName, perform: { _ in + .onChange(of: channelName) { 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 - }) + } } HStack { Picker("Key Size", selection: $channelKeySize) { @@ -98,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 { @@ -107,7 +106,7 @@ struct ChannelForm: View { hasValidKey = false } hasChanges = true - }) + } .disabled(channelKeySize <= 0) } HStack { @@ -130,7 +129,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,24 +138,26 @@ 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==" && channelKeySize > 1) && 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 = 14 + } } } } - if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 10...19, step: 1) { + + Slider(value: $positionPrecision, in: 11...14, step: 1) { } minimumValueLabel: { Image(systemName: "minus") } maximumValueLabel: { @@ -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,40 +196,52 @@ struct ChannelForm: View { } hasChanges = true } - .onChange(of: channelKey) { _ in + .onChange(of: channelKey) { hasChanges = true } - .onChange(of: channelRole) { _ in + .onChange(of: channelKeySize) { + if channelKeySize == -1 { + if channelRole == 0 { + preciseLocation = false + } + channelKey = "AQ==" + } + } + .onChange(of: channelRole) { hasChanges = true } - .onChange(of: preciseLocation) { loc in + .onChange(of: preciseLocation) { _, loc in if loc == true { - positionPrecision = 32 + if channelKey == "AQ==" || channelKeySize <= 1 { + preciseLocation = false + } else { + positionPrecision = 32 + } } else { positionPrecision = 14 } 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 = 32 + positionPrecision = 14 } } else { positionPrecision = 0 } hasChanges = true } - .onChange(of: uplink) { _ in + .onChange(of: uplink) { hasChanges = true } - .onChange(of: downlink) { _ in + .onChange(of: downlink) { hasChanges = true } - .onAppear { + .onFirstAppear { let tempKey = Data(base64Encoded: channelKey) ?? Data() if tempKey.count == channelKeySize || channelKeySize == -1 { hasValidKey = true diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index b43813c2..d57dacba 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() @@ -46,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: "") @@ -60,7 +59,7 @@ struct BluetoothConfig: View { } else if fixedPin.utf8.count < pinLength { shortPin = true } - }) + } .foregroundColor(.gray) } .keyboardType(.decimalPad) @@ -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,43 +100,43 @@ struct BluetoothConfig: View { ) } ) - .onAppear { - setBluetoothValues() + .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") + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 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) + } + } } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.bluetoothConfig != nil { - if newEnabled != node!.bluetoothConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != 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) { oldNode, newNode in + if oldNode != newNode && newNode != 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 } - } - } - .onChange(of: deviceLoggingEnabled) { newDeviceLogging in - if node != nil && node!.bluetoothConfig != nil { - if newDeviceLogging != node!.bluetoothConfig!.deviceLoggingEnabled { hasChanges = true } - } + .onChange(of: fixedPin) { oldFixedPin, newFixedPin in + if oldFixedPin != newFixedPin && newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { 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 } } diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index 3ff815f8..d1af3a6a 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -17,17 +17,19 @@ 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 { Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .onFirstAppear(onAppear) .font(.title3) - .onAppear(perform: onAppear) } } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 { Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .onFirstAppear(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..da2978cf 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -23,13 +23,11 @@ struct DeviceConfig: View { @State var deviceRole = 0 @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 @State var ledHeartbeatEnabled = true - @State var isManaged = false + @State var tripleClickAsAdHocPing = true @State var tzdef = "" var body: some View { @@ -62,12 +60,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 { @@ -85,6 +77,12 @@ struct DeviceConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $tripleClickAsAdHocPing) { + 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)) + Toggle(isOn: $ledHeartbeatEnabled) { Label("LED Heartbeat", systemImage: "waveform.path.ecg") Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.") @@ -92,28 +90,20 @@ struct DeviceConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("Debug")) { - Toggle(isOn: $serialEnabled) { - 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") TextField("Time Zone", text: $tzdef, axis: .vertical) .foregroundColor(.gray) - .onChange(of: tzdef, perform: { _ in - let totalBytes = tzdef.utf8.count + .onChange(of: tzdef) { + 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) @@ -155,7 +145,7 @@ struct DeviceConfig: View { .disabled(node?.user == nil) .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.leading) .confirmationDialog( "are.you.sure", @@ -180,10 +170,10 @@ 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.", + "All device and app data will be deleted.", isPresented: $isPresentingFactoryResetConfirm, titleVisibility: .visible ) { @@ -206,20 +196,14 @@ struct DeviceConfig: View { if connectedNode != nil { 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() dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress - dc.isManaged = isManaged + dc.disableTripleClick = !tripleClickAsAdHocPing 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 @@ -233,76 +217,69 @@ 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 : "?") - }) - .onAppear { - setDeviceValues() - // Need to request a LoRaConfig 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) - if node != nil && connectedNode != nil && connectedNode?.user != nil { - _ = bleManager.requestDeviceConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a DeviceConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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.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) + } + } + } } } } - .onChange(of: deviceRole) { newRole in - if node != nil && node?.deviceConfig != nil { - if newRole != node!.deviceConfig!.role { hasChanges = true } - } + .onChange(of: deviceRole) { oldRole, newRole in + if oldRole != newRole && newRole != 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: buttonGPIO) { oldButtonGPIO, newButtonGPIO in + if oldButtonGPIO != newButtonGPIO && newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } - .onChange(of: debugLogEnabled) { newDebugLog in - if node != nil && node?.deviceConfig != nil { - if newDebugLog != node!.deviceConfig!.debugLogEnabled { hasChanges = true } - } + .onChange(of: buzzerGPIO) { oldBuzzerGPIO, newBuzzerGPIO in + if oldBuzzerGPIO != newBuzzerGPIO && newBuzzerGPIO != node?.deviceConfig?.buzzerGpio ?? -1 { hasChanges = true } } - .onChange(of: buttonGPIO) { newButtonGPIO in - if node != nil && node?.deviceConfig != nil { - if newButtonGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } - } + .onChange(of: rebroadcastMode) { oldRebroadcastMode, newRebroadcastMode in + if oldRebroadcastMode != newRebroadcastMode && newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true } } - .onChange(of: buzzerGPIO) { newBuzzerGPIO in - if node != nil && node?.deviceConfig != nil { - if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } - } + .onChange(of: nodeInfoBroadcastSecs) { oldNodeInfoBroadcastSecs, newNodeInfoBroadcastSecs in + if oldNodeInfoBroadcastSecs != newNodeInfoBroadcastSecs && newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true } } - .onChange(of: rebroadcastMode) { newRebroadcastMode in - if node != nil && node?.deviceConfig != nil { - if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true } - } + .onChange(of: doubleTapAsButtonPress) { oldDoubleTapAsButtonPress, newDoubleTapAsButtonPress in + if oldDoubleTapAsButtonPress != newDoubleTapAsButtonPress && newDoubleTapAsButtonPress != node?.deviceConfig?.doubleTapAsButtonPress ?? false { hasChanges = true } } - .onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in - if node != nil && node?.deviceConfig != nil { - if newNodeInfoBroadcastSecs != node!.deviceConfig!.nodeInfoBroadcastSecs { hasChanges = true } - } + .onChange(of: tripleClickAsAdHocPing) { oldTripleClickAsAdHocPing, newTripleClickAsAdHocPing in + if oldTripleClickAsAdHocPing != newTripleClickAsAdHocPing && newTripleClickAsAdHocPing != node?.deviceConfig?.tripleClickAsAdHocPing ?? false { hasChanges = true } } - .onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in - if node != nil && node?.deviceConfig != nil { - if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true } - } + .onChange(of: tzdef) { oldTzdef, newTzdef in + if oldTzdef != newTzdef && newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } } - .onChange(of: isManaged) { newIsManaged in - if node != nil && node?.deviceConfig != nil { - if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true } - } - } - .onChange(of: tzdef) { newTzdef in - if node != nil && node?.deviceConfig != nil { - if newTzdef != node!.deviceConfig!.tzdef { hasChanges = true } - } + .onChange(of: ledHeartbeatEnabled) { oldLedHeartbeatEnabled, newLedHeartbeatEnabled in + if oldLedHeartbeatEnabled != newLedHeartbeatEnabled && newLedHeartbeatEnabled != node?.deviceConfig?.ledHeartbeatEnabled ?? false { hasChanges = true } } } 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) @@ -311,8 +288,8 @@ struct DeviceConfig: View { nodeInfoBroadcastSecs = 3600 } self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false + self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? 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/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index f800ebb6..88839956 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -154,66 +154,63 @@ struct DisplayConfig: View { } .navigationTitle("display.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? 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") - 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) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a DisplayConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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.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) + } + } } } } - .onChange(of: screenOnSeconds) { newScreenSecs in - if node != nil && node!.displayConfig != nil { - if newScreenSecs != node!.displayConfig!.screenOnSeconds { hasChanges = true } - } + .onChange(of: screenOnSeconds) { oldScreenSecs, newScreenSecs in + if oldScreenSecs != newScreenSecs && 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 } - } + .onChange(of: screenCarouselInterval) { oldCarouselSecs, newCarouselSecs in + if oldCarouselSecs != newCarouselSecs && newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true } } - .onChange(of: compassNorthTop) { newCompassNorthTop in - if node != nil && node!.displayConfig != nil { - if newCompassNorthTop != node!.displayConfig!.compassNorthTop { hasChanges = true } - } + .onChange(of: compassNorthTop) { oldCompassNorthTop, newCompassNorthTop in + if oldCompassNorthTop != newCompassNorthTop && newCompassNorthTop != 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) { oldWakeOnTapOrMotion, newWakeOnTapOrMotion in + if oldWakeOnTapOrMotion != newWakeOnTapOrMotion && newWakeOnTapOrMotion != 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: gpsFormat) { oldGpsFormat, newGpsFormat in + if oldGpsFormat != newGpsFormat && newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true } } - .onChange(of: flipScreen) { newFlipScreen in - if node != nil && node!.displayConfig != nil { - if newFlipScreen != 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 node != nil && node!.displayConfig != nil { - if newOledType != node!.displayConfig!.oledType { hasChanges = true } - } + .onChange(of: oledType) { oldOledType, newOledType in + if oldOledType != newOledType && 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 } - } + .onChange(of: displayMode) { oldDisplayMode, newDisplayMode in + if oldDisplayMode != newDisplayMode && 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 } - } + .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 c98e162c..51461cf0 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -45,10 +45,13 @@ 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() formatter.numberStyle = .decimal + formatter.allowsFloats = true + formatter.maximumFractionDigits = 4 return formatter }() @@ -98,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") @@ -207,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 } @@ -221,85 +229,79 @@ struct LoRaConfig: View { } } .navigationTitle("lora.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) - .onAppear { - setLoRaValues() + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { // 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") - 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) + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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.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) + } + } } } } - .onChange(of: region) { newRegion in - if node != nil && node!.loRaConfig != nil { - if newRegion != node!.loRaConfig!.regionCode { hasChanges = true } - } + .onChange(of: region) { _, newRegion in + if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true } } - .onChange(of: usePreset) { newUsePreset in - if node != nil && node!.loRaConfig != nil { - if newUsePreset != node!.loRaConfig!.usePreset { hasChanges = true } - } + .onChange(of: usePreset) { _, newPreset in + if newPreset != node?.loRaConfig?.usePreset { hasChanges = true } } - .onChange(of: modemPreset) { newModemPreset in - if node != nil && node!.loRaConfig != nil { - if newModemPreset != node!.loRaConfig!.modemPreset { hasChanges = true } - } + .onChange(of: modemPreset) { _, newModemPreset in + 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 } - } + .onChange(of: hopLimit) { _, newHopLimit in + 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 } - } + .onChange(of: channelNum) { _, newChannelNum in + 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 } - } + .onChange(of: bandwidth) { _, newBandwidth in + 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 } - } + .onChange(of: codingRate) { _, newCodingRate in + 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 } - } + .onChange(of: spreadFactor) { _, newSpreadFactor in + if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true } } - .onChange(of: rxBoostedGain) { newRxBoostedGain in - if node != nil && node!.loRaConfig != nil { - if newRxBoostedGain != node!.loRaConfig!.sx126xRxBoostedGain { hasChanges = true } - } + .onChange(of: rxBoostedGain) { _, newRxBoostedGain in + if newRxBoostedGain != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true } } - .onChange(of: overrideFrequency) { newOverrideFrequency in - if node != nil && node!.loRaConfig != nil { - if newOverrideFrequency != node!.loRaConfig!.overrideFrequency { hasChanges = true } - } + .onChange(of: overrideFrequency) { _, newOverrideFrequency in + 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: txPower) { _, newTxPower in + if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true } } - .onChange(of: txEnabled) { newTxEnabled in - if node != nil && node!.loRaConfig != nil { - if newTxEnabled != node!.loRaConfig!.txEnabled { hasChanges = true } - } + .onChange(of: txEnabled) { _, newTxEnabled in + if newTxEnabled != 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) { _, newIgnoreMqtt in + if newIgnoreMqtt != node?.loRaConfig?.ignoreMqtt { hasChanges = true } + } + .onChange(of: okToMqtt) { _, newOkToMqtt in + if newOkToMqtt != node?.loRaConfig?.okToMqtt { hasChanges = true } } } func setLoRaValues() { @@ -316,6 +318,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/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 3fe5560d..62340da8 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -6,8 +6,8 @@ // import MeshtasticProtobufs import SwiftUI +import OSLog -@available(iOS 17.0, macOS 14.0, *) struct AmbientLightingConfig: View { @Environment(\.self) var environment @Environment(\.managedObjectContext) var context @@ -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) @@ -80,24 +76,45 @@ 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 : "?") - }) - .onAppear { - setAmbientLightingConfigValue() + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .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 { + 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 { + 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) + } + } } } } - .onChange(of: ledState) { newLedState in - if node != nil && node!.ambientLightingConfig != nil { - if newLedState != node!.ambientLightingConfig!.ledState { hasChanges = true } - } + .onChange(of: ledState) { _, newLedState in + if newLedState != node?.ambientLightingConfig?.ledState { hasChanges = true } + } + .onChange(of: current) { _, newCurrent in + if newCurrent != node?.ambientLightingConfig?.current ?? 10 { 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 670e24e1..b5dfee63 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -71,15 +71,15 @@ struct CannedMessagesConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: messages, perform: { _ in - - let totalBytes = messages.utf8.count + .onChange(of: messages) { + 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 - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -224,22 +224,38 @@ 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 : "?") - }) - .onAppear { - setCannedMessagesValues() + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .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 { + 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 { + 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) + } + } } } } - .onChange(of: configPreset) { newPreset in + .onChange(of: configPreset) { _, newPreset in if newPreset == 1 { @@ -268,55 +284,35 @@ 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) { _, newEnabled in + if newEnabled != node?.cannedMessageConfig?.enabled { hasChanges = true } } - .onChange(of: sendBell) { newBell in - if node != nil && node!.cannedMessageConfig != nil { - if newBell != node!.cannedMessageConfig!.sendBell { hasChanges = true } - } + .onChange(of: sendBell) { _, newSendBell in + if newSendBell != node?.cannedMessageConfig?.sendBell { hasChanges = true } } - .onChange(of: rotary1Enabled) { newRot1 in - if node != nil && node!.cannedMessageConfig != nil { - if newRot1 != node!.cannedMessageConfig!.rotary1Enabled { hasChanges = true } - } + .onChange(of: rotary1Enabled) { _, newRotary1Enabled in + if newRotary1Enabled != node?.cannedMessageConfig?.rotary1Enabled { hasChanges = true } } - .onChange(of: updown1Enabled) { newUpDown in - if node != nil && node!.cannedMessageConfig != nil { - if newUpDown != node!.cannedMessageConfig!.updown1Enabled { hasChanges = true } - } + .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 480c4e24..59ee4f3f 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 @@ -91,14 +91,14 @@ struct DetectionSensorConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: name, perform: { _ in - - let totalBytes = name.utf8.count + .onChange(of: name) { + 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 } - }) + } } .listRowSeparator(.hidden) Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"") @@ -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)") + Picker("TriggerType", selection: $triggerType) { + ForEach(TriggerTypes.allCases) { tt in + Text(tt.name).tag(tt.rawValue) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .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) @@ -180,62 +182,62 @@ 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 : "?") - }) - .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") - 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) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a DetectionSensorModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node?.detectionSensorConfig != nil { - if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.detectionSensorConfig?.enabled { hasChanges = true } } - .onChange(of: sendBell) { newSendBell in - if node != nil && node?.detectionSensorConfig != nil { - if newSendBell != node!.detectionSensorConfig!.sendBell { hasChanges = true } - } + .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: triggerType) { _, newTriggerType in + if newTriggerType != node?.detectionSensorConfig?.triggerType ?? 0 { hasChanges = true } } - .onChange(of: usePullup) { newUsePullup in - if node != nil && node?.detectionSensorConfig != nil { - if newUsePullup != node!.detectionSensorConfig!.usePullup { hasChanges = true } - } + .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 } } @@ -245,7 +247,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) diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index abdca8a2..9602c44b 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -190,95 +190,81 @@ 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 : "?") - }) - .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") - 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) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a ExternalNotificationModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.externalNotificationConfig != nil { - if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.externalNotificationConfig?.enabled { hasChanges = true } } - .onChange(of: alertBell) { newAlertBell in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } - } + .onChange(of: alertBell) { _, newAlertBell in + if newAlertBell != node?.externalNotificationConfig?.alertBell { hasChanges = true } } - .onChange(of: alertBellBuzzer) { newAlertBellBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBellBuzzer != node!.externalNotificationConfig!.alertBellBuzzer { hasChanges = true } - } + .onChange(of: alertBellBuzzer) { _, newAlertBellBuzzer in + if newAlertBellBuzzer != node?.externalNotificationConfig?.alertBellBuzzer { hasChanges = true } } - .onChange(of: alertBellVibra) { newAlertBellVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBellVibra != node!.externalNotificationConfig!.alertBellVibra { hasChanges = true } - } + .onChange(of: alertBellVibra) { _, newAlertBellVibra in + if newAlertBellVibra != node?.externalNotificationConfig?.alertBellVibra { hasChanges = true } } - .onChange(of: alertMessage) { newAlertMessage in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } - } + .onChange(of: alertMessage) { _, newAlertMessage in + if newAlertMessage != node?.externalNotificationConfig?.alertMessage { hasChanges = true } } - .onChange(of: alertMessageBuzzer) { newAlertMessageBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessageBuzzer != node!.externalNotificationConfig!.alertMessageBuzzer { hasChanges = true } - } + .onChange(of: alertMessageBuzzer) { _, newAlertMessageBuzzer in + if newAlertMessageBuzzer != node?.externalNotificationConfig?.alertMessageBuzzer { hasChanges = true } } - .onChange(of: alertMessageVibra) { newAlertMessageVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessageVibra != node!.externalNotificationConfig!.alertMessageVibra { hasChanges = true } - } + .onChange(of: alertMessageVibra) { _, newAlertMessageVibra in + if newAlertMessageVibra != node?.externalNotificationConfig?.alertMessageVibra { hasChanges = true } } - .onChange(of: active) { newActive in - if node != nil && node!.externalNotificationConfig != nil { - if newActive != node!.externalNotificationConfig!.active { hasChanges = true } - } + .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) { newUsePWM in - if node != nil && node!.externalNotificationConfig != nil { - if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } - } + .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) { newUseI2SAsBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newUseI2SAsBuzzer != node!.externalNotificationConfig!.useI2SAsBuzzer { hasChanges = true } - } + .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 f923e560..fc93e6f9 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 @@ -32,8 +32,7 @@ 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 + @State var mapPositionPrecision: Double = 14.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) } } } @@ -142,13 +123,14 @@ struct MQTTConfig: View { Label("Root Topic", systemImage: "tree") TextField("Root Topic", text: $root) .foregroundColor(.gray) - .onChange(of: root, perform: { _ in - let totalBytes = root.utf8.count + .onChange(of: root) { + 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) } .keyboardType(.asciiCapable) @@ -180,14 +162,15 @@ struct MQTTConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: address, perform: { _ in - let totalBytes = address.utf8.count + .onChange(of: address) { + 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 - }) + } .keyboardType(.default) } .autocorrectionDisabled() @@ -198,16 +181,15 @@ struct MQTTConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: username, perform: { _ in - - let totalBytes = username.utf8.count - + .onChange(of: username) { + 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 - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -218,15 +200,15 @@ struct MQTTConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: password, perform: { _ in - - let totalBytes = password.utf8.count + .onChange(of: password) { + 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 - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -234,7 +216,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)) } @@ -271,68 +253,56 @@ 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) - }) - .onChange(of: address) { newAddress in - if node != nil && node?.mqttConfig != nil { - if newAddress != node!.mqttConfig!.address { hasChanges = true } + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) } + ) + .onChange(of: enabled) { _, newEnabled in + if newEnabled != node?.mqttConfig?.enabled { hasChanges = true } } - .onChange(of: username) { newUsername in - if node != nil && node?.mqttConfig != nil { - 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 } - } - } - .onChange(of: root) { newRoot in - if node != nil && node?.mqttConfig != nil { - if newRoot != node!.mqttConfig!.root { hasChanges = true } - } - } - .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 + .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 - } - } + if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true } } - .onChange(of: encryptionEnabled) { newEncryptionEnabled in - if node != nil && node?.mqttConfig != nil { - if newEncryptionEnabled != node!.mqttConfig!.encryptionEnabled { hasChanges = true } - } + .onChange(of: address) { _, newAddress in + if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true } } - .onChange(of: jsonEnabled) { newJsonEnabled in + .onChange(of: username) { newUsername in + if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true } + } + .onChange(of: password) { newPassword in + if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true } + } + .onChange(of: root) { newRoot in + if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true } + } + .onChange(of: selectedTopic) { _, newSelectedTopic in + root = newSelectedTopic + } + .onChange(of: encryptionEnabled) { _, newEncryptionEnabled in + if newEncryptionEnabled != 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 address.lowercased() == "mqtt.meshtastic.org" { + tlsEnabled = false + } else { + if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true } } } - .onChange(of: tlsEnabled) { newTlsEnabled in - if node != nil && node?.mqttConfig != nil { - 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() @@ -343,79 +313,81 @@ struct MQTTConfig: View { } } } - .onChange(of: mapReportingEnabled) { newMapReportingEnabled in - if node != nil && node?.mqttConfig != nil { - if newMapReportingEnabled != node!.mqttConfig!.mapReportingEnabled { hasChanges = true } - } + .onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in + if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true } } - .onChange(of: preciseLocation) { _ in - hasChanges = true + .onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in + if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true } } - .onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in - if node != nil && node?.mqttConfig != nil { - if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true } - } - } - .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") - 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) + .onFirstAppear { + // Need to request a MqttModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } } } 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)) + defaultTopic = "msh/" + (region?.topic ?? "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 your region is a country + if !(region?.isCountry ?? false) { + 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 ?? "" @@ -428,11 +400,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 } } diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index d7670a7c..24af3504 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,25 +58,33 @@ struct PaxCounterConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) - .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) - 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 { + 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 { + 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) + } + } } } } - .onChange(of: enabled) { - if let val = node?.paxCounterConfig?.enabled { - hasChanges = $0 != val - } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != node?.paxCounterConfig?.enabled { hasChanges = true } } - .onChange(of: paxcounterUpdateInterval) { - if let val = node?.paxCounterConfig?.updateInterval { - hasChanges = $0 != val - } + .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/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 0bf99d65..979eb736 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -72,35 +72,45 @@ 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 : "?") - }) - .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") - 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) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a RangeTestModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.rangeTestConfig != nil { - if newEnabled != node!.rangeTestConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { _, newEnabled in + if newEnabled != 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) { _, newSave in + if newSave != 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) { _, 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 3128fffe..b81e2348 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 @@ -30,14 +31,14 @@ struct RtttlConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: ringtone, perform: { _ in - - let totalBytes = ringtone.utf8.count + .onChange(of: ringtone) { + 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) } .keyboardType(.default) @@ -62,21 +63,38 @@ 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 : "?") - }) - .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) - if node != nil && connectedNode != nil { - _ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a RtttlConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } } - .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/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 813e1328..fd2c0c99 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -127,78 +127,60 @@ struct SerialConfig: View { } } .navigationTitle("serial.config") - .navigationBarItems(trailing: - - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) - .onAppear { - setSerialValues() + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .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 { + 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 { + 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) + } + } } } - } - .onChange(of: enabled) { newEnabled in - - if node != nil && node!.serialConfig != nil { - - if newEnabled != node!.serialConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { oldEnabled, newEnabled in + if oldEnabled != newEnabled && newEnabled != node?.serialConfig?.enabled ?? false { hasChanges = true } } - .onChange(of: echo) { newEcho in - - if node != nil && node!.serialConfig != nil { - - if newEcho != 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 e73380f3..9c247acf 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 { @@ -137,50 +137,54 @@ struct StoreForwardConfig: View { } } .navigationTitle("storeforward.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? 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 { - 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) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { + // Need to request a StoreForwardModuleConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } - setStoreAndForwardValues() } - .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 ?? true { hasChanges = true } } - .onChange(of: records) { newRecords in - if node != nil && node?.storeForwardConfig != nil { - if newRecords != node!.storeForwardConfig!.records { hasChanges = true } - } + .onChange(of: records) { oldRecords, newRecords in + if oldRecords != newRecords && newRecords != node!.storeForwardConfig?.records ?? -1 { hasChanges = true } } - .onChange(of: historyReturnMax) { newHistoryReturnMax in - if node != nil && node?.storeForwardConfig != nil { - if newHistoryReturnMax != node!.storeForwardConfig!.historyReturnMax { hasChanges = true } - } + .onChange(of: historyReturnMax) { oldHistoryReturnMax, newHistoryReturnMax in + if oldHistoryReturnMax != newHistoryReturnMax && newHistoryReturnMax != node!.storeForwardConfig?.historyReturnMax ?? -1 { hasChanges = true } } - .onChange(of: historyReturnWindow) { newHistoryReturnWindow in - if node != nil && node?.storeForwardConfig != nil { - if newHistoryReturnWindow != node!.storeForwardConfig!.historyReturnWindow { hasChanges = true } - } + .onChange(of: historyReturnWindow) { oldHistoryReturnWindow, newHistoryReturnWindow in + if oldHistoryReturnWindow != newHistoryReturnWindow && newHistoryReturnWindow != node!.storeForwardConfig?.historyReturnWindow ?? -1 { hasChanges = true } } } func setStoreAndForwardValues() { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index a1f827b7..ad173dae 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -125,60 +125,60 @@ struct TelemetryConfig: View { } } .navigationTitle("telemetry.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) - .onAppear { - setTelemetryValues() + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onFirstAppear { // 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") - 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) + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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 { + 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) + } + } } } } - .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 } } } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index d969eab9..84f48c41 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -45,14 +45,15 @@ struct NetworkConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: wifiSsid, perform: { _ in - let totalBytes = wifiSsid.utf8.count + .onChange(of: wifiSsid) { + 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 - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -62,14 +63,15 @@ struct NetworkConfig: View { .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: wifiPsk, perform: { _ in - let totalBytes = wifiPsk.utf8.count + .onChange(of: wifiPsk) { + 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 - }) + } .foregroundColor(.gray) } .keyboardType(.default) @@ -109,12 +111,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") @@ -124,30 +130,42 @@ struct NetworkConfig: View { } } } - .onChange(of: wifiEnabled) { newEnabled in - if node != nil && node!.networkConfig != nil { - if newEnabled != node!.networkConfig!.wifiEnabled { hasChanges = true } + .onFirstAppear { + // Need to request a NetworkConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { + 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.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) + } + } + } } } - .onChange(of: wifiSsid) { newSSID in - if node != nil && node!.networkConfig != nil { - if newSSID != node!.networkConfig!.wifiSsid { hasChanges = true } - } + .onChange(of: wifiEnabled) { _, newEnabled in + if newEnabled != node?.networkConfig?.wifiEnabled { hasChanges = true } } - .onChange(of: wifiPsk) { newPsk in - if node != nil && node!.networkConfig != nil { - if newPsk != node!.networkConfig!.wifiPsk { hasChanges = true } - } + .onChange(of: wifiSsid) { _, newSSID in + if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true } } - .onChange(of: wifiMode) { newMode in - if node != nil && node!.networkConfig != nil { - if newMode != node!.networkConfig!.wifiMode { hasChanges = true } - } + .onChange(of: wifiPsk) { _, newPsk in + if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true } } - .onChange(of: ethEnabled) { newEthEnabled in - if node != nil && node!.networkConfig != nil { - if newEthEnabled != node!.networkConfig!.ethEnabled { hasChanges = true } - } + .onChange(of: wifiMode) { _, newMode in + if newMode != node?.networkConfig?.wifiMode ?? -1 { 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 aa6960a0..f963b02b 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 } + } } } } @@ -376,23 +406,30 @@ struct PositionConfig: View { ) } ) - .onAppear { - setPositionValues() + .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 { - Logger.mesh.info("empty position config") + // Need to request a NetworkConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { 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 node.num != connectedNode.num { + if UserDefaults.enableAdministration { + /// 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) + } + } } } } - .onChange(of: fixedPosition) { newFixed in + .onChange(of: fixedPosition) { _, newFixed in if supportedVersion { if let positionConfig = node?.positionConfig { /// Fixed Position is off to start @@ -405,51 +442,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() { - guard let positionConfig = node?.positionConfig else { return } + func handlePositionFlagtChanges() { + guard (node?.positionConfig) != nil 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 d8de7e44..e9f7c0e5 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 @@ -117,9 +118,8 @@ struct PowerConfig: View { .font(.subheadline) } } - .onAppear { + .onFirstAppear { Api().loadDeviceHardwareData { (hw) in - for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") @@ -128,51 +128,53 @@ struct PowerConfig: View { } } } - setPowerValues() + // Need to request a NetworkConfig from the remote node before allowing changes + if let connectedPeripheral = bleManager.connectedPeripheral, let node { - // 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) + 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.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) + } + } } } } - .onChange(of: isPowerSaving) { - if let val = node?.powerConfig?.isPowerSaving { - hasChanges = $0 != val + .onChange(of: isPowerSaving) { oldIsPowerSaving, newIsPowerSaving in + if oldIsPowerSaving != newIsPowerSaving && newIsPowerSaving != node?.powerConfig?.isPowerSaving { hasChanges = true } + } + .onChange(of: shutdownOnPowerLoss) { _, newShutdownOnPowerLoss in + if newShutdownOnPowerLoss { + hasChanges = true } } - .onChange(of: shutdownOnPowerLoss) { _ in + .onChange(of: shutdownAfterSecs) { oldShutdownAfterSecs, newShutdownAfterSecs in + if oldShutdownAfterSecs != newShutdownAfterSecs && newShutdownAfterSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } + } + .onChange(of: adcOverride) { hasChanges = true } - .onChange(of: shutdownAfterSecs) { - if let val = node?.powerConfig?.onBatteryShutdownAfterSecs { - hasChanges = $0 != val - } + .onChange(of: adcMultiplier) { _, newAdcMultiplier in + if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true } } - .onChange(of: adcOverride) { _ in - hasChanges = true + .onChange(of: waitBluetoothSecs) { oldWaitBluetoothSecs, newWaitBluetoothSecs in + if oldWaitBluetoothSecs != newWaitBluetoothSecs && newWaitBluetoothSecs != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true } } - .onChange(of: adcMultiplier) { - if let val = node?.powerConfig?.adcMultiplierOverride { - hasChanges = $0 != val - } + .onChange(of: lsSecs) { _, newLsSecs in + if newLsSecs != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true } } - .onChange(of: waitBluetoothSecs) { - if let val = node?.powerConfig?.waitBluetoothSecs { - hasChanges = $0 != val - } - } - .onChange(of: lsSecs) { - if let val = node?.powerConfig?.lsSecs { - hasChanges = $0 != val - } - } - .onChange(of: minWakeSecs) { - if let val = node?.powerConfig?.minWakeSecs { - hasChanges = $0 != val - } + .onChange(of: minWakeSecs) { _, newMinWakeSecs in + if newMinWakeSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { @@ -232,13 +234,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 new file mode 100644 index 00000000..6f16c8ff --- /dev/null +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -0,0 +1,271 @@ +// +// Security.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/7/24. +// + +import Foundation +import SwiftUI +import CoreData +import MeshtasticProtobufs +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 + + var node: NodeInfoEntity? + + @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 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 + @State var adminChannelEnabled = false + + var body: some 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") + 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) + Divider() + Label("Private Key", systemImage: "key.fill") + 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) + 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: $adminKey3, isValid: $hasValidAdminKey3) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The tertiary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } + } + Section(header: Text("Logs")) { + 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)) + } + 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, the user is unable to access any of the device settings.") + } + .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)) + } + } + } + .scrollDismissesKeyboard(.immediately) + .navigationTitle("Security Config") + .navigationBarItems(trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" + ) + }) + .onChange(of: isManaged) { _, newIsManaged in + if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true } + } + .onChange(of: serialEnabled) { _, newSerialEnabled in + if newSerialEnabled != node?.securityConfig?.serialEnabled { hasChanges = true } + } + .onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in + if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } + } + .onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in + if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true } + } + .onChange(of: publicKey) { + let tempKey = Data(base64Encoded: publicKey) ?? Data() + if tempKey.count == 32 { + hasValidPublicKey = true + } else { + hasValidPublicKey = false + } + hasChanges = true + } + .onChange(of: privateKey) { + let tempKey = Data(base64Encoded: privateKey) ?? Data() + if tempKey.count == 32 { + hasValidPrivateKey = true + } else { + hasValidPrivateKey = false + } + hasChanges = true + } + .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 + } + 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 { + 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.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) + } + } + } + } + } + } + + SaveConfigButton(node: node, hasChanges: $hasChanges) { + + if !hasValidPublicKey || !hasValidPrivateKey || !hasValidAdminKey { + return + } + + 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 = Data(base64Encoded: publicKey) ?? Data() + config.privateKey = Data(base64Encoded: privateKey) ?? Data() + config.adminKey = [Data(base64Encoded: adminKey) ?? Data(), Data(base64Encoded: adminKey2) ?? Data(), Data(base64Encoded: adminKey3) ?? Data()] + config.isManaged = isManaged + config.serialEnabled = serialEnabled + config.debugLogApiEnabled = debugLogApiEnabled + 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() ?? "" + self.adminKey = 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 + self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false + self.hasChanges = false + } +} diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 87f2bb88..76922d54 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.5.4" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? @@ -148,7 +148,7 @@ struct Firmware: View { VStack(alignment: .leading) { Text("ESP32 Device Firmware Update") .font(.title3) - Text("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.") + Text("Currently the recommended 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.") .font(.caption) Link("Web Flasher", destination: URL(string: "https://flash.meshtastic.org")!) .font(.callout) @@ -185,7 +185,7 @@ struct Firmware: View { } .padding() .padding(.bottom, 5) - .onAppear { + .onFirstAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" 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/Logs/LogDetail.swift b/Meshtastic/Views/Settings/Logs/LogDetail.swift index 8006127e..ca5d17d6 100644 --- a/Meshtastic/Views/Settings/Logs/LogDetail.swift +++ b/Meshtastic/Views/Settings/Logs/LogDetail.swift @@ -37,11 +37,13 @@ struct LogDetail: View { List { /// Time Label { - Text("log.time".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.date.formatted(dateFormatStyle)) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.time".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.date.formatted(dateFormatStyle)) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "timer") .symbolRenderingMode(.hierarchical) @@ -53,11 +55,13 @@ struct LogDetail: View { .listSectionSeparator(.visible, edges: .bottom) /// Subsystem Label { - Text("log.subsystem".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.subsystem) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.subsystem".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.subsystem) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "gear") .symbolRenderingMode(.hierarchical) @@ -68,11 +72,13 @@ struct LogDetail: View { .listRowSeparator(.visible) /// Process Label { - Text("log.process".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.process) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.process".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.process) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "tag") .symbolRenderingMode(.hierarchical) @@ -83,12 +89,13 @@ struct LogDetail: View { .listRowSeparator(.visible) /// Level Label { - Text("log.level".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.level.description) - .font(idiom == .phone ? .caption : .title) - .foregroundStyle(log.level.color) + HStack { + Text("log.level".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.level.description) + .font(idiom == .phone ? .caption : .title) + .foregroundStyle(log.level.color) } } icon: { Image(systemName: "stairs") .symbolRenderingMode(.hierarchical) @@ -99,11 +106,13 @@ struct LogDetail: View { .listRowSeparator(.visible) /// Category Label { - Text("log.category".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.category) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.category".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.category) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "square.grid.2x2") .symbolRenderingMode(.hierarchical) 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/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index c19dbd9f..82e7bb32 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 @@ -284,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 cc70de3b..174221d2 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 @@ -175,14 +174,14 @@ struct Routes: View { axis: .vertical ) .foregroundColor(Color.gray) - .onChange(of: name, perform: { _ in - let totalBytes = name.utf8.count + .onChange(of: name) { + 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 } - }) + } 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/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 faad71ad..ab8c97b3 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 @@ -17,6 +15,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 @@ -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") @@ -146,13 +154,11 @@ 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") } } @@ -243,21 +249,11 @@ struct Settings: View { var loggingSection: some View { Section(header: Text("logging")) { - NavigationLink(value: SettingsNavigationState.meshLog) { + NavigationLink(value: SettingsNavigationState.debugLogs) { Label { - Text("mesh.log") + Text("Logs") } icon: { - Image(systemName: "list.bullet.rectangle") - } - } - - if #available (iOS 17.4, *) { - NavigationLink(value: SettingsNavigationState.debugLogs) { - Label { - Text("Debug Logs") - } icon: { - Image(systemName: "stethoscope") - } + Image(systemName: "scroll") } } } @@ -265,6 +261,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") @@ -292,13 +295,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 } ) ) { @@ -319,37 +319,33 @@ 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") } } - let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 + 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") { - if hasAdmin { - Picker("Configuring Node", selection: $selectedNode) { + if node?.canRemoteAdmin ?? false { + Picker("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)") @@ -357,16 +353,31 @@ struct Settings: View { Image(systemName: "antenna.radiowaves.left.and.right") } .tag(Int(node.num)) - } else if node.metadata != nil { + } 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") + } + .font(.caption2) + .tag(Int(node.num)) + } else if !UserDefaults.enableAdministration && node.metadata != nil { /// Nodes using the old admin system + Label { + Text("Remote Legacy Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { Image(systemName: "av.remote") } .tag(Int(node.num)) - } else if hasAdmin { + } 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") } @@ -374,14 +385,13 @@ struct Settings: View { } } } - .pickerStyle(.automatic) - .labelsHidden() - .onChange(of: selectedNode) { newValue in + .pickerStyle(.navigationLink) + .onChange(of: selectedNode) { _, newValue in if selectedNode > 0 { 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") @@ -389,9 +399,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)") @@ -417,13 +425,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: @@ -431,56 +435,54 @@ 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, *) { - AmbientLightingConfig(node: node) - } + 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 .security: + SecurityConfig(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: - if #available(iOS 17.4, *) { - AppLog() - } + AppLog() case .appFiles: AppData() case .firmwareUpdates: 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 136b9563..1e10c571 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() @@ -55,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) @@ -240,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 c67c70bf..daacb849 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -49,13 +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 - let totalBytes = longName.utf8.count + .onChange(of: longName) { + 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 } - }) + } } .keyboardType(.default) .disableAutocorrection(true) @@ -73,13 +74,14 @@ struct UserConfig: View { Label("Short Name", systemImage: "circlebadge.fill") TextField("Short Name", text: $shortName) .foregroundColor(.gray) - .onChange(of: shortName, perform: { _ in - let totalBytes = shortName.utf8.count + .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) @@ -195,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 @@ -217,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/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index ba263709..15510b87 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/admin.proto @@ -24,11 +25,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 { +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. + /// + /// 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 +376,67 @@ public struct AdminMessage { 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)} + } + + /// + /// 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)} + } + + /// + /// 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) @@ -390,6 +458,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 +511,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)} } /// @@ -456,7 +534,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) @@ -563,6 +641,25 @@ public struct AdminMessage { /// 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) + /// + /// 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) + /// + /// 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) @@ -570,6 +667,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,191 +684,17 @@ 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) - #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 /// @@ -798,6 +724,15 @@ public struct AdminMessage { /// /// TODO: REPLACE case bluetoothConfig // = 6 + + /// + /// TODO: REPLACE + case securityConfig // = 7 + case sessionkeyConfig // = 8 + + /// + /// device-ui config + case deviceuiConfig // = 9 case UNRECOGNIZED(Int) public init() { @@ -813,6 +748,9 @@ public struct AdminMessage { case 4: self = .displayConfig case 5: self = .loraConfig case 6: self = .bluetoothConfig + case 7: self = .securityConfig + case 8: self = .sessionkeyConfig + case 9: self = .deviceuiConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -826,15 +764,32 @@ public struct AdminMessage { case .displayConfig: return 4 case .loraConfig: return 5 case .bluetoothConfig: return 6 + case .securityConfig: return 7 + case .sessionkeyConfig: return 8 + case .deviceuiConfig: return 9 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, + .deviceuiConfig, + ] + } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum { + public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -932,50 +887,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 +941,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 +955,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" @@ -1035,6 +962,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"), @@ -1068,13 +996,20 @@ 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"), + 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"), 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"), ] @@ -1413,6 +1348,64 @@ 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 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 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) @@ -1429,6 +1422,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 +1467,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 { @@ -1477,6 +1478,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .nodedbReset(v) } }() + case 101: try { try decoder.decodeSingularBytesField(value: &self.sessionPasskey) }() default: break } } @@ -1620,6 +1622,30 @@ 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 .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 .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) @@ -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 { @@ -1654,10 +1684,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 @@ -1673,6 +1707,9 @@ 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"), + 8: .same(proto: "SESSIONKEY_CONFIG"), + 9: .same(proto: "DEVICEUI_CONFIG"), ] } @@ -1725,7 +1762,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..52dac5ca 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/apponly.proto @@ -7,7 +8,6 @@ // 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 @@ -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..06d6af88 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/atak.proto @@ -20,7 +21,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 +131,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 +149,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 +228,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 +240,12 @@ extension MemberRole: CaseIterable { .rto, .k9, ] -} -#endif // swift(>=4.2) +} /// /// Packets for the official ATAK Plugin -public struct TAKPacket { +public struct TAKPacket: @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. @@ -322,36 +311,33 @@ public struct TAKPacket { set {payloadVariant = .chat(newValue)} } + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender (EUD) + public var detail: Data { + get { + if case .detail(let v)? = payloadVariant {return v} + return Data() + } + set {payloadVariant = .detail(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TAK position report case pli(PLI) /// /// ATAK GeoChat message case chat(GeoChat) + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender (EUD) + case detail(Data) - #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 +349,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 +391,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 +413,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 +430,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 +450,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 +482,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" @@ -555,6 +529,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 +568,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 +607,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/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..ce1f0503 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/cannedmessages.proto @@ -7,7 +8,6 @@ // 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 @@ -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..180cd698 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/channel.proto @@ -36,13 +37,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 +114,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 +135,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 +173,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 +212,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 +226,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..d72c0ae1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/clientonly.proto @@ -7,7 +8,6 @@ // 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 @@ -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. @@ -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,12 +125,11 @@ 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) -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" @@ -110,6 +142,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 +158,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 +186,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 +204,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/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index f396367c..c8c90be7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/config.proto @@ -20,7 +21,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. @@ -85,11 +86,35 @@ 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 sessionkey: Config.SessionkeyConfig { + get { + if case .sessionkey(let v)? = payloadVariant {return v} + return Config.SessionkeyConfig() + } + 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() /// /// 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) @@ -97,50 +122,15 @@ public struct Config { case display(Config.DisplayConfig) case lora(Config.LoRaConfig) 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 { - // 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. @@ -151,12 +141,10 @@ public struct Config { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI - public var serialEnabled: Bool = false - + /// Moved to SecurityConfig /// - /// 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 + /// 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 @@ -184,6 +172,9 @@ 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 /// @@ -202,7 +193,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 +211,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 +303,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 /// @@ -336,6 +344,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() { @@ -348,6 +365,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) } } @@ -358,10 +377,22 @@ 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 } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + .none, + .corePortnumsOnly, + ] + } public init() {} @@ -369,7 +400,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 +422,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 +434,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 +476,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 +566,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 +621,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 +636,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 +696,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 +743,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 +775,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 +816,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 +872,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 +935,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 +981,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 +1035,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 +1089,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 +1167,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 +1186,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. @@ -1234,9 +1341,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 /// @@ -1314,6 +1428,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() { @@ -1341,6 +1467,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) } } @@ -1366,16 +1495,45 @@ 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 } } + // 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, + .ph433, + .ph868, + .ph915, + ] + } /// /// 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 /// @@ -1388,6 +1546,9 @@ 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 /// @@ -1409,6 +1570,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() { @@ -1425,6 +1592,7 @@ public struct Config { case 5: self = .shortSlow case 6: self = .shortFast case 7: self = .longModerate + case 8: self = .shortTurbo default: self = .UNRECOGNIZED(rawValue) } } @@ -1439,10 +1607,24 @@ 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 } } + // 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() {} @@ -1450,7 +1632,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. @@ -1467,13 +1649,9 @@ public struct Config { /// Specified PIN for PairingMode.FixedPin public var fixedPin: UInt32 = 0 - /// - /// Enables device (serial style logs) over Bluetooth - 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 /// @@ -1511,207 +1689,75 @@ 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: @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. + + /// + /// 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] = [] + + /// + /// 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 or bluetooth is set to true. + public var debugLogApiEnabled: Bool = false + + /// + /// Allow incoming device control over the insecure legacy admin channel. + public var adminChannelEnabled: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + 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, - ] -} - -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" @@ -1726,6 +1772,9 @@ extension Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBas 5: .same(proto: "display"), 6: .same(proto: "lora"), 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 { @@ -1825,6 +1874,45 @@ 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) + } + }() + 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) + } + }() + 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 } } @@ -1864,6 +1952,18 @@ 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 .sessionkey?: try { + 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) @@ -1881,7 +1981,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"), @@ -1901,7 +2000,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) }() @@ -1923,9 +2021,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) } @@ -1959,7 +2054,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} @@ -1996,6 +2090,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"), ] } @@ -2168,7 +2264,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 { @@ -2503,6 +2599,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 { @@ -2523,6 +2620,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. @@ -2554,6 +2652,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem _paFanDisabled = source._paFanDisabled _ignoreIncoming = source._ignoreIncoming _ignoreMqtt = source._ignoreMqtt + _configOkToMqtt = source._configOkToMqtt } } @@ -2589,6 +2688,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 } } @@ -2612,7 +2712,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 +2736,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 { @@ -2648,6 +2748,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) } @@ -2674,6 +2777,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} @@ -2704,6 +2808,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"), ] } @@ -2717,6 +2824,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"), ] } @@ -2726,7 +2834,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 { @@ -2738,7 +2845,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 } } @@ -2754,9 +2860,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) } @@ -2764,7 +2867,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 } @@ -2777,3 +2879,90 @@ 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"), + 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.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 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.visitRepeatedBytesField(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.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.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..6847c0e3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/connection_status.proto @@ -7,7 +8,6 @@ // 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 @@ -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/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift new file mode 100644 index 00000000..82c6e834 --- /dev/null +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -0,0 +1,722 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// 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, Swift.CaseIterable { + 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 + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Theme] = [ + .dark, + .light, + .red, + ] + +} + +/// +/// Localization +public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { + 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 + + /// + /// Norwegian + case norwegian // = 14 + + /// + /// 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 14: self = .norwegian + 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 .norwegian: return 14 + case .simplifiedChinese: return 30 + case .traditionalChinese: return 31 + case .UNRECOGNIZED(let i): return i + } + } + + // 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, + .norwegian, + .simplifiedChinese, + .traditionalChinese, + ] + +} + +public struct DeviceUIConfig: @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. + + /// + /// 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} + + /// + /// 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() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +public struct NodeFilter: Sendable { + // SwiftProtobuf.Message 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: Sendable { + // SwiftProtobuf.Message 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() {} +} + +// 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"), + 14: .same(proto: "NORWEGIAN"), + 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"), + 14: .standard(proto: "calibration_data"), + ] + + 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 + var _calibrationData: Data = Data() + + #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 + _calibrationData = source._calibrationData + } + } + + 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) }() + case 14: try { try decoder.decodeSingularBytesField(value: &_storage._calibrationData) }() + 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) + } }() + if !_storage._calibrationData.isEmpty { + try visitor.visitSingularBytesField(value: _storage._calibrationData, fieldNumber: 14) + } + } + 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} + if _storage._calibrationData != rhs_storage._calibrationData {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 10b9af2b..a8f57eaf 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/deviceonly.proto @@ -20,64 +21,9 @@ 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 { +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 +58,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 +119,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. @@ -188,11 +181,15 @@ 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} + 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 @@ -202,6 +199,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() {} @@ -215,7 +227,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 +287,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 +337,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. @@ -343,89 +357,10 @@ 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 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 = [ @@ -482,6 +417,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 = [ @@ -495,19 +498,23 @@ 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 { var _num: UInt32 = 0 - var _user: User? = nil + var _user: UserLite? = nil var _position: PositionLite? = nil var _snr: Float = 0 var _lastHeard: UInt32 = 0 var _deviceMetrics: DeviceMetrics? = nil var _channel: UInt32 = 0 var _viaMqtt: Bool = false - var _hopsAway: UInt32 = 0 + 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. @@ -532,6 +539,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway _isFavorite = source._isFavorite + _isIgnored = source._isIgnored + _nextHop = source._nextHop } } @@ -560,6 +569,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 } } @@ -581,7 +592,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 { @@ -596,12 +607,18 @@ 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) } + 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) } @@ -621,6 +638,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} @@ -815,81 +834,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/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 5e30d1cd..c3356286 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/localonly.proto @@ -7,7 +8,6 @@ // 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 @@ -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. @@ -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() {} @@ -118,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. @@ -282,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" @@ -302,6 +308,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 +320,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 +343,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati _lora = source._lora _bluetooth = source._bluetooth _version = source._version + _security = source._security } } @@ -361,6 +370,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 +407,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 +427,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 57e8bde4..72ba0edc 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/mesh.proto @@ -25,7 +26,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 /// @@ -126,6 +127,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 +202,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 /// @@ -321,6 +326,78 @@ 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 + + /// + /// 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 + + /// + /// 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, 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 + + /// + /// WisMesh Tap + /// RAK-4631 w/ TFT in injection modled case + case wismeshTap // = 84 + + /// + /// Similar to PORTDUINO but used by Routastic devices, this is not any + /// particular device and does not run Meshtastic's code but supports + /// the same frame format. + /// Runs on linux, see https://github.com/Jorropo/routastic + case routastic // = 85 + + /// + /// Mesh-Tab, esp32 based + /// https://github.com/valzzu/Mesh-Tab + case meshTab // = 86 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -358,6 +435,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 @@ -403,6 +481,23 @@ 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 72: self = .rak3172 + 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 79: self = .rpiPico2 + case 80: self = .m5StackCores3 + case 81: self = .seeedXiaoS3 + case 82: self = .ms24Sf1 + case 83: self = .tloraC6 + case 84: self = .wismeshTap + case 85: self = .routastic + case 86: self = .meshTab case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -434,6 +529,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 @@ -479,16 +575,28 @@ 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 .rak3172: return 72 + 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 .rpiPico2: return 79 + case .m5StackCores3: return 80 + case .seeedXiaoS3: return 81 + case .ms24Sf1: return 82 + case .tloraC6: return 83 + case .wismeshTap: return 84 + case .routastic: return 85 + case .meshTab: return 86 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, @@ -515,6 +623,7 @@ extension HardwareModel: CaseIterable { .wioWm1110, .rak2560, .heltecHru3601, + .heltecWirelessBridge, .stationG1, .rak11310, .senseloraRp2040, @@ -560,15 +669,31 @@ extension HardwareModel: CaseIterable { .heltecVisionMasterE213, .heltecVisionMasterE290, .heltecMeshNodeT114, + .sensecapIndicator, + .trackerT1000E, + .rak3172, + .wioE5, + .radiomaster900Bandit, + .me25Ls014Y10Td, + .rp2040FeatherRfm95, + .m5StackCorebasic, + .m5StackCore2, + .rpiPico2, + .m5StackCores3, + .seeedXiaoS3, + .ms24Sf1, + .tloraC6, + .wismeshTap, + .routastic, + .meshTab, .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 /// @@ -580,7 +705,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() { @@ -590,7 +715,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) } } @@ -598,31 +723,25 @@ 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 } } -} - -#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 /// @@ -674,6 +793,17 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { /// 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() { @@ -694,6 +824,8 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { case 9: self = .brownout case 10: self = .sx1262Failure case 11: self = .radioSpiBug + case 12: self = .flashCorruptionRecoverable + case 13: self = .flashCorruptionUnrecoverable default: self = .UNRECOGNIZED(rawValue) } } @@ -712,15 +844,12 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { 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, @@ -735,14 +864,143 @@ extension CriticalErrorCode: CaseIterable { .brownout, .sx1262Failure, .radioSpiBug, + .flashCorruptionRecoverable, + .flashCorruptionUnrecoverable, ] + } -#endif // swift(>=4.2) +/// +/// 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, Swift.CaseIterable { + 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 + } + } + + // 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, + ] + +} /// -/// a gps position -public struct Position { +/// A GPS 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. @@ -751,23 +1009,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 @@ -810,16 +1080,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 @@ -863,16 +1141,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) @@ -931,7 +1217,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 /// @@ -975,12 +1261,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 /// @@ -1030,6 +1324,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() {} @@ -1037,31 +1340,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. @@ -1083,7 +1361,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. @@ -1108,6 +1386,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() /// @@ -1127,22 +1407,39 @@ 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() {} } /// -/// A message used in our Dynamic Source Routing protocol (RFC 4728 based) -public struct RouteDiscovery { +/// A message used in a traceroute +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. /// - /// 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() {} @@ -1150,7 +1447,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. @@ -1190,7 +1487,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) @@ -1202,34 +1499,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 /// @@ -1281,6 +1556,22 @@ 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 + + /// + /// 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() { @@ -1301,6 +1592,10 @@ public struct Routing { case 9: self = .dutyCycleLimit case 32: self = .badRequest case 33: self = .notAuthorized + case 34: self = .pkiFailed + case 35: self = .pkiUnknownPubkey + case 36: self = .adminBadSessionKey + case 37: self = .adminPublicKeyUnauthorized default: self = .UNRECOGNIZED(rawValue) } } @@ -1319,42 +1614,44 @@ public struct Routing { case .dutyCycleLimit: return 9 case .badRequest: return 32 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 } } + // 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, + .adminBadSessionKey, + .adminPublicKeyUnauthorized, + ] + } 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. @@ -1401,14 +1698,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. @@ -1419,11 +1729,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) @@ -1449,11 +1773,14 @@ public struct Waypoint { 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 { +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. @@ -1494,7 +1821,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) @@ -1502,24 +1829,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() {} @@ -1529,7 +1838,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. @@ -1622,7 +1931,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. @@ -1663,6 +1972,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} @@ -1683,9 +1994,39 @@ 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} + } + + /// + /// 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 { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -1693,24 +2034,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 } /// @@ -1732,7 +2055,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 /// @@ -1757,6 +2080,19 @@ 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 + + /// + /// Higher priority alert message used for critical alerts which take priority over other reliable packets. + case alert // = 110 + /// /// Ack/naks are sent with very high priority to ensure that retransmission /// stops as soon as possible @@ -1778,6 +2114,9 @@ public struct MeshPacket { case 10: self = .background case 64: self = .default case 70: self = .reliable + case 80: self = .response + case 100: self = .high + case 110: self = .alert case 120: self = .ack case 127: self = .max default: self = .UNRECOGNIZED(rawValue) @@ -1791,17 +2130,34 @@ public struct MeshPacket { case .background: return 10 case .default: return 64 case .reliable: return 70 + case .response: return 80 + case .high: return 100 + case .alert: return 110 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, + .alert, + .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 /// @@ -1839,6 +2195,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() {} @@ -1846,32 +2209,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... @@ -1889,7 +2226,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. @@ -1965,11 +2302,15 @@ 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} + 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 @@ -1979,6 +2320,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() {} @@ -1990,7 +2339,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: @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. @@ -2010,6 +2359,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() {} @@ -2021,7 +2378,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. @@ -2046,7 +2403,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 /// @@ -2108,29 +2465,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. @@ -2157,7 +2508,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. @@ -2319,11 +2670,31 @@ 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)} + } + + /// + /// 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() /// /// 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) @@ -2374,81 +2745,61 @@ public struct FromRadio { /// /// File system manifest messages case fileInfo(FileInfo) + /// + /// 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 { - // 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() {} } +/// +/// 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: Sendable { + // SwiftProtobuf.Message 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 { +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. @@ -2469,7 +2820,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. @@ -2549,7 +2900,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) @@ -2576,40 +2927,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() {} @@ -2617,7 +2934,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. @@ -2637,7 +2954,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. @@ -2665,7 +2982,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. @@ -2695,7 +3012,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. @@ -2740,6 +3057,15 @@ public struct DeviceMetadata { /// Has Remote Hardware enabled public var hasRemoteHardware_p: Bool = false + /// + /// 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() {} @@ -2748,7 +3074,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. @@ -2760,7 +3086,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. @@ -2787,7 +3113,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. @@ -2815,7 +3141,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. @@ -2829,7 +3155,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. @@ -2872,7 +3198,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) @@ -2883,75 +3209,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" @@ -2982,6 +3244,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"), @@ -3027,6 +3290,23 @@ 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"), + 72: .same(proto: "RAK3172"), + 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"), + 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"), + 84: .same(proto: "WISMESH_TAP"), + 85: .same(proto: "ROUTASTIC"), + 86: .same(proto: "MESH_TAB"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3034,7 +3314,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"), ] } @@ -3052,6 +3332,27 @@ 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"), + ] +} + +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"), ] } @@ -3084,22 +3385,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 @@ -3193,15 +3494,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) } @@ -3217,12 +3522,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) } @@ -3235,12 +3540,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) } @@ -3332,6 +3637,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 { @@ -3347,6 +3653,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 } } @@ -3374,6 +3681,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) } @@ -3385,6 +3695,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 } @@ -3394,6 +3705,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 { @@ -3403,6 +3717,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 } } @@ -3412,11 +3729,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 } @@ -3519,6 +3848,10 @@ 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"), + 36: .same(proto: "ADMIN_BAD_SESSION_KEY"), + 37: .same(proto: "ADMIN_PUBLIC_KEY_UNAUTHORIZED"), ] } @@ -3533,6 +3866,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 { @@ -3549,12 +3883,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) } @@ -3579,6 +3918,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) } @@ -3591,6 +3933,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 } @@ -3616,8 +3959,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) }() @@ -3629,15 +3972,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) } @@ -3658,8 +4005,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} @@ -3760,6 +4107,10 @@ 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"), + 18: .standard(proto: "next_hop"), + 19: .standard(proto: "relay_node"), ] fileprivate class _StorageClass { @@ -3777,6 +4128,10 @@ 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 + 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. @@ -3805,6 +4160,10 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio _delayed = source._delayed _viaMqtt = source._viaMqtt _hopStart = source._hopStart + _publicKey = source._publicKey + _pkiEncrypted = source._pkiEncrypted + _nextHop = source._nextHop + _relayNode = source._relayNode } } @@ -3857,6 +4216,10 @@ 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) }() + case 18: try { try decoder.decodeSingularUInt32Field(value: &_storage._nextHop) }() + case 19: try { try decoder.decodeSingularUInt32Field(value: &_storage._relayNode) }() default: break } } @@ -3895,7 +4258,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 { @@ -3919,6 +4282,18 @@ 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) + } + 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) } @@ -3942,6 +4317,10 @@ 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} + if _storage._nextHop != rhs_storage._nextHop {return false} + if _storage._relayNode != rhs_storage._relayNode {return false} return true } if !storagesAreEqual {return false} @@ -3958,6 +4337,9 @@ 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"), + 110: .same(proto: "ALERT"), 120: .same(proto: "ACK"), 127: .same(proto: "MAX"), ] @@ -3984,6 +4366,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 { @@ -3995,8 +4378,9 @@ 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 + var _isIgnored: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -4021,6 +4405,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _viaMqtt = source._viaMqtt _hopsAway = source._hopsAway _isFavorite = source._isFavorite + _isIgnored = source._isIgnored } } @@ -4049,6 +4434,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 } } @@ -4070,7 +4456,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 { @@ -4085,12 +4471,15 @@ 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) } + if _storage._isIgnored != false { + try visitor.visitSingularBoolField(value: _storage._isIgnored, fieldNumber: 11) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4110,6 +4499,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} @@ -4125,6 +4515,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 { @@ -4136,6 +4528,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 } } @@ -4151,6 +4545,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) } @@ -4158,6 +4558,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 } @@ -4293,6 +4695,8 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 13: .same(proto: "metadata"), 14: .same(proto: "mqttClientProxyMessage"), 15: .same(proto: "fileInfo"), + 16: .same(proto: "clientNotification"), + 17: .same(proto: "deviceuiConfig"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4474,6 +4878,32 @@ 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) + } + }() + 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 } } @@ -4544,6 +4974,14 @@ 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 .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) @@ -4557,6 +4995,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 = [ @@ -4843,7 +5335,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 { @@ -4878,6 +5370,8 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 8: .standard(proto: "position_flags"), 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 { @@ -4896,6 +5390,8 @@ 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) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.excludedModules) }() default: break } } @@ -4932,6 +5428,12 @@ 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) + } + if self.excludedModules != 0 { + try visitor.visitSingularUInt32Field(value: self.excludedModules, fieldNumber: 12) + } try unknownFields.traverse(visitor: &visitor) } @@ -4946,6 +5448,8 @@ 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.excludedModules != rhs.excludedModules {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4956,8 +5460,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..2cb3291b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/module_config.proto @@ -7,7 +8,6 @@ // 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 @@ -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. @@ -455,9 +387,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() {} @@ -465,7 +402,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. @@ -475,13 +412,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 +439,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,12 +449,76 @@ public struct ModuleConfig { public var unknownFields = SwiftProtobuf.UnknownStorage() + public enum TriggerType: SwiftProtobuf.Enum, Swift.CaseIterable { + 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 + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ + .logicLow, + .logicHigh, + .fallingEdge, + .risingEdge, + .eitherEdgeActiveLow, + .eitherEdgeActiveHigh, + ] + + } + public init() {} } /// /// 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 +555,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 +602,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 +622,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 +648,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 +691,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 +759,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 +828,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 +846,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 +929,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 +965,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 +990,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. @@ -980,20 +1026,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() {} @@ -1001,7 +1059,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. @@ -1044,7 +1102,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() /// @@ -1056,7 +1114,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1124,6 +1182,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 +1202,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 +1235,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 +1259,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" @@ -1745,6 +1722,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 { @@ -1755,6 +1733,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 } } @@ -1767,12 +1746,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 } @@ -1787,7 +1770,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 +1786,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 +1812,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 +1828,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 +2320,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 +2341,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 +2380,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 +2403,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/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index efe6cdd5..006fd9c8 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/mqtt.proto @@ -7,7 +8,6 @@ // 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 @@ -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..e24ed371 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/paxcount.proto @@ -7,7 +8,6 @@ // 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 @@ -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..79dfd7f1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/portnums.proto @@ -7,7 +8,6 @@ // 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 @@ -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 /// @@ -107,6 +107,10 @@ public enum PortNum: SwiftProtobuf.Enum { /// NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 case detectionSensorApp // = 10 + /// + /// Same as Text Message but used for critical alerts. + case alertApp // = 11 + /// /// Provides a 'ping' service that replies to any packet it receives. /// Also serves as a small example module. @@ -167,7 +171,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 @@ -222,6 +226,7 @@ public enum PortNum: SwiftProtobuf.Enum { case 8: self = .waypointApp case 9: self = .audioApp case 10: self = .detectionSensorApp + case 11: self = .alertApp case 32: self = .replyApp case 33: self = .ipTunnelApp case 34: self = .paxcounterApp @@ -256,6 +261,7 @@ public enum PortNum: SwiftProtobuf.Enum { case .waypointApp: return 8 case .audioApp: return 9 case .detectionSensorApp: return 10 + case .alertApp: return 11 case .replyApp: return 32 case .ipTunnelApp: return 33 case .paxcounterApp: return 34 @@ -277,11 +283,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, @@ -295,6 +296,7 @@ extension PortNum: CaseIterable { .waypointApp, .audioApp, .detectionSensorApp, + .alertApp, .replyApp, .ipTunnelApp, .paxcounterApp, @@ -313,14 +315,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 { @@ -336,6 +333,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 8: .same(proto: "WAYPOINT_APP"), 9: .same(proto: "AUDIO_APP"), 10: .same(proto: "DETECTION_SENSOR_APP"), + 11: .same(proto: "ALERT_APP"), 32: .same(proto: "REPLY_APP"), 33: .same(proto: "IP_TUNNEL_APP"), 34: .same(proto: "PAXCOUNTER_APP"), diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 5f51e948..58c21701 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/powermon.proto @@ -7,7 +8,6 @@ // 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 @@ -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..d23dc07b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/remote_hardware.proto @@ -7,7 +8,6 @@ // 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 @@ -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..38d0c880 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/rtttl.proto @@ -7,7 +8,6 @@ // 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 @@ -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..deb96569 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/storeforward.proto @@ -22,7 +23,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 +80,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 +94,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 +217,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 +289,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 +314,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 +335,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..737ebf95 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/telemetry.proto @@ -7,7 +8,6 @@ // 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 @@ -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,42 @@ 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 + + /// + /// 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 + + /// + /// SCD40/SCD41 CO2, humidity, temperature sensor + case scd4X // = 32 + + /// + /// ClimateGuard RadSens, radiation, Geiger-Muller Tube + case radsens // = 33 + + /// + /// High accuracy current and voltage + case ina226 // = 34 case UNRECOGNIZED(Int) public init() { @@ -162,6 +198,15 @@ 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 + case 29: self = .customSensor + case 30: self = .max30102 + case 31: self = .mlx90614 + case 32: self = .scd4X + case 33: self = .radsens + case 34: self = .ina226 default: self = .UNRECOGNIZED(rawValue) } } @@ -194,15 +239,19 @@ 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 .customSensor: return 29 + case .max30102: return 30 + case .mlx90614: return 31 + case .scd4X: return 32 + case .radsens: return 33 + case .ina226: return 34 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,46 +280,95 @@ extension TelemetrySensorType: CaseIterable { .aht10, .dfrobotLark, .nau7802, + .bmp3Xx, + .icm20948, + .max17048, + .customSensor, + .max30102, + .mlx90614, + .scd4X, + .radsens, + .ina226, ] -} -#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. /// /// 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 { +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. @@ -278,123 +376,202 @@ 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} + + /// + /// 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() @@ -405,103 +582,368 @@ 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. /// /// 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 { +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. /// /// 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} + + /// + /// 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() {} + + 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 + fileprivate var _co2: UInt32? = nil +} + +/// +/// Local device mesh statistics +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. + + /// + /// 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 (both good and bad) + 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 + + /// + /// 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: Sendable { + // SwiftProtobuf.Message 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 { +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. @@ -552,9 +994,29 @@ public struct Telemetry { 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)} + } + + /// + /// 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 { + public enum OneOf_Variant: Equatable, Sendable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -567,33 +1029,13 @@ public struct Telemetry { /// /// Power Metrics case powerMetrics(PowerMetrics) + /// + /// 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 { - // 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 +1043,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 +1061,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" @@ -662,6 +1093,15 @@ 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"), + 29: .same(proto: "CUSTOM_SENSOR"), + 30: .same(proto: "MAX30102"), + 31: .same(proto: "MLX90614"), + 32: .same(proto: "SCD4X"), + 33: .same(proto: "RADSENS"), + 34: .same(proto: "INA226"), ] } @@ -681,41 +1121,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 } @@ -741,26 +1185,28 @@ 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 { - 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 + var _radiation: Float? = nil #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -792,6 +1238,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple _weight = source._weight _windGust = source._windGust _windLull = source._windLull + _radiation = source._radiation } } @@ -827,6 +1274,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 } } @@ -835,57 +1283,64 @@ 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 { if let v = _storage._radiation { + try visitor.visitSingularFloatField(value: v, fieldNumber: 18) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -912,6 +1367,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} @@ -938,46 +1394,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 } @@ -998,6 +1458,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 { @@ -1006,76 +1467,225 @@ 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) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self._co2) }() 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 { if let v = self._co2 { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 13) + } }() 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._co2 != rhs._co2 {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"), + 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 { + 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) }() + 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 + } + } + } + + public func traverse(visitor: inout V) throws { + if self.uptimeSeconds != 0 { + try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) + } + if self.channelUtilization.bitPattern != 0 { + try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 2) + } + if self.airUtilTx.bitPattern != 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) + } + 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) + } + + 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.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 } @@ -1089,6 +1699,8 @@ 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"), + 7: .standard(proto: "health_metrics"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1150,6 +1762,32 @@ 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) + } + }() + 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 } } @@ -1180,6 +1818,14 @@ 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 .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) @@ -1217,7 +1863,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..46907a58 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/xmodem.proto @@ -20,7 +21,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 +36,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 +80,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/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) } } diff --git a/README.md b/README.md index f9fbbd5e..bbaf49b0 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ # Meshtastic Apple Clients - - Meshtastic App Store Launch Image - - ## Overview 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. @@ -28,9 +24,7 @@ open Meshtastic.xcworkspace ### Supported Operating Systems -* iOS 16+ -* iPadOS 16+ -* macOS 13+ +The last two major operating system versions are supported on iOS, iPadOS and macOS. ### Code Standards @@ -39,11 +33,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/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 diff --git a/Widgets/MeshActivityAttributes.swift b/Widgets/MeshActivityAttributes.swift index 916377c9..37376531 100644 --- a/Widgets/MeshActivityAttributes.swift +++ b/Widgets/MeshActivityAttributes.swift @@ -4,7 +4,7 @@ // // Created by Garth Vander Houwen on 3/1/23. // - +#if !targetEnvironment(macCatalyst) #if canImport(ActivityKit) import ActivityKit @@ -15,13 +15,20 @@ 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 dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 + + public var numTxRelayCanceled: UInt32 = 0 + var timerRange: ClosedRange } // Fixed non-changing properties about your activity go here! @@ -29,3 +36,4 @@ struct MeshActivityAttributes: ActivityAttributes { var name: String } #endif +#endif diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 396aaac9..7c6396a3 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -13,71 +13,87 @@ 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) - .widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)")) + 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, + dupeReceivedPackets: context.state.dupeReceivedPackets, + packetsSentRelay: context.state.packetsSentRelay, + packetsCanceledRelay: context.state.packetsCanceledRelay, + nodesOnline: context.state.nodesOnline, + totalNodes: context.state.totalNodes, + timerRange: context.state.timerRange) + .widgetURL(URL(string: "meshtastic:///bluetooth")) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - Text("Network") - .font(.headline) - .fontWeight(.bold) - .foregroundStyle(.secondary) - .fixedSize() - .padding(.top, 10) + if context.state.totalNodes >= 100 { + Text("100+ online") + .font(.callout) + .foregroundStyle(.secondary) + .fixedSize() + } else { + Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online") + .font(.callout) + .foregroundStyle(.secondary) + .fixedSize() + } Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") - .font(.headline) - .fontWeight(.medium) + .font(.caption2) .foregroundStyle(.secondary) .fixedSize() Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") - .font(.headline) - .fontWeight(.medium) + .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() - 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) - } - } - } - DynamicIslandExpandedRegion(.trailing, priority: 1) { TimerView(timerRange: context.state.timerRange) .tint(Color("LightIndigo")) - + } + DynamicIslandExpandedRegion(.trailing, priority: 1) { + 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(context.attributes.name) - .font(context.attributes.name.count > 14 ? .callout : .title3) - .fontWeight(.semibold) - .foregroundStyle(.tint) Text("Last Heard: \(Date().formatted())") - .font(.caption) + .font(.caption2) .fontWeight(.medium) - .foregroundStyle(.secondary) + .foregroundStyle(.tint) .fixedSize() } } compactLeading: { Image("m-logo-black") .resizable() - .frame(width: 30.0) + .frame(width: 25) .padding(4) .background(.green.gradient, in: ContainerRelativeShape()) } compactTrailing: { @@ -95,15 +111,14 @@ 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")) } } } 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 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 @@ -120,60 +135,41 @@ struct WidgetsLiveActivity_Previews: PreviewProvider { .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 dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 var timerRange: ClosedRange 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(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, + dupeReceivedPackets: dupeReceivedPackets, packetsSentRelay: packetsSentRelay, packetsCanceledRelay: packetsCanceledRelay, 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]) - .padding(.trailing, 32) + .padding(.trailing, 25) .activityBackgroundTint(colorScheme == .light ? Color("LiveActivityBackground") : Color("AccentColorDimmed")) .activitySystemActionForegroundColor(.primary) } @@ -183,37 +179,59 @@ 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 dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 + var nodesOnline: UInt32 + var totalNodes: UInt32 + var timerRange: ClosedRange var body: some View { + let errorRate = (Double(badReceivedPackets) / Double(receivedPackets)) * 100 VStack(alignment: .leading, spacing: 0) { Text(nodeName) .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) Rec. \(receivedPackets)") + .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() + Text("Bad: \(badReceivedPackets) \(String(format: "Error Rate: %.2f", errorRate))%") + .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) @@ -255,8 +273,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) @@ -268,10 +287,12 @@ struct TimerView: View { .fontWeight(.semibold) .foregroundStyle(.tint) Image(systemName: "timer") + .symbolRenderingMode(.multicolor) .resizable() .foregroundStyle(.secondary) .frame(width: 30, height: 30) .opacity(isLuminanceReduced ? 0.5 : 1.0) + .offset(y: -5) } } } diff --git a/protobufs b/protobufs index 10494bf3..2cffaf53 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 10494bf328ac051fc4add9ddeb677eebf337b531 +Subproject commit 2cffaf53e3faf1b6e41a8b8f05312f2f893be413 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." 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 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