diff --git a/.github/workflows/macos-dSYM.yml b/.github/workflows/macos-dSYM.yml index cb490792..adf1194e 100644 --- a/.github/workflows/macos-dSYM.yml +++ b/.github/workflows/macos-dSYM.yml @@ -1,5 +1,10 @@ name: Upload dSYM Files +on: + push: + branches: [ main ] + workflow_dispatch: + jobs: build: runs-on: macos-latest @@ -8,14 +13,70 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Generate/Download dSYM Files - uses: ./release.sh + - name: Select Xcode Version + run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + + - name: Show Xcode Version + run: xcodebuild -version + + - name: Setup Environment Variables + env: + DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} + run: | + echo "DATADOG_CLIENT_TOKEN=${DATADOG_CLIENT_TOKEN}" >> $GITHUB_ENV + + - name: Build iOS App and Generate dSYMs + env: + DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} + run: | + # Create build directory + mkdir -p ./build/dSYMs + + # Build iOS App Archive with dSYMs + xcodebuild \ + -workspace Meshtastic.xcworkspace \ + -scheme Meshtastic \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -archivePath ./build/Meshtastic.xcarchive \ + DATADOG_CLIENT_TOKEN="${DATADOG_CLIENT_TOKEN}" \ + DEBUG_INFORMATION_FORMAT=dwarf-with-dsym \ + DWARF_DSYM_FOLDER_PATH=./build/dSYMs \ + archive + + - name: Extract dSYMs from Archive + run: | + # Find and copy all dSYM files from the archive + find ./build/Meshtastic.xcarchive -name "*.dSYM" -exec cp -R {} ./build/dSYMs/ \; + + # List what we found + echo "Found dSYM files:" + find ./build/dSYMs -name "*.dSYM" -type d + - name: Install Datadog CI + run: | + npm install -g @datadog/datadog-ci + - name: Upload dSYMs to Datadog - uses: DataDog/upload-dsyms-github-action@v1 + env: + DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }} + DATADOG_SITE: datadoghq.com + run: | + # Upload all dSYM files to Datadog + if [ -d "./build/dSYMs" ] && [ "$(find ./build/dSYMs -name "*.dSYM" -type d | wc -l)" -gt 0 ]; then + echo "Uploading dSYM files to Datadog..." + datadog-ci dsyms upload ./build/dSYMs --dry-run=false + else + echo "No dSYM files found to upload" + exit 1 + fi + + - name: Upload Build Artifacts + uses: actions/upload-artifact@v4 + if: always() with: - api_key: ${{ secrets.DATADOG_API_KEY }} - site: datadoghq.com - dsym_paths: | - path/to/dsyms/folder - path/to/zip/dsyms.zip \ No newline at end of file + name: dsym-files + path: | + ./build/dSYMs + ./build/Meshtastic.xcarchive + retention-days: 30 \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 9471fd63..bc0b61f6 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -28,8 +28,8 @@ file_length: # TODO: should review cyclomatic_complexity: - warning: 60 - error: 70 + warning: 70 + error: 80 ignores_case_statements: true # TODO: should review @@ -57,4 +57,4 @@ custom_rules: name: "Disable `print()`" regex: "((\\bprint)|(Swift\\.print))\\s*\\(" message: "Consider using a dedicated log message or the Xcode debugger instead of using `print`. ex. logger.debug(...)" - severity: warning \ No newline at end of file + severity: warning diff --git a/Localizable.xcstrings b/Localizable.xcstrings index c04d1475..8c321273 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -670,7 +670,14 @@ } }, "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ конфигурациони подаци су захтевани преко ПКЦ администратора, али није добијен одговор са удаљеног чвора." + } + } + } }, "%@ dB" : { "localizations" : { @@ -1611,7 +1618,8 @@ "value" : "0" } } - } + }, + "shouldTranslate" : false }, "1" : { "localizations" : { @@ -1621,7 +1629,8 @@ "value" : "1" } } - } + }, + "shouldTranslate" : false }, "1 byte" : { "localizations" : { @@ -1794,6 +1803,12 @@ "state" : "translated", "value" : "12時間表示" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "12-часовни сат" + } } } }, @@ -1975,7 +1990,8 @@ "value" : "180" } } - } + }, + "shouldTranslate" : false }, "256 bit" : { "localizations" : { @@ -2012,7 +2028,14 @@ } }, "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Индекс канала 0 означава примарни канал са кога се шаљу емитовани пакети. Подаци о локацији се емитују са првог канала где је то омогућено почевши од фирмвера 2.7 па надаље." + } + } + } }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { @@ -2021,6 +2044,12 @@ "state" : "translated", "value" : "緑色の鍵は、チャンネルが128ビットまたは256ビットのAESキーで安全に暗号化されていることを意味します。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зелени катанац означава да је канал безбедно шифрован кључем од 128 или 256 бита AES." + } } } }, @@ -2089,10 +2118,24 @@ } }, "A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Црвени отворени катанац означава да канал није безбедно шифрован и користи се за прецизне податке о локацији, при чему се или не користи кључ или се користи познати кључ од 1 бајта." + } + } + } }, "A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Црвени отворени катанац са упозорењем означава да канал није безбедно шифрован и користи се за прецизне податке о локацији који се преносе на интернет преко MQTT-а, при чему се или не користи кључ или се користи познати кључ од 1 бајта." + } + } + } }, "A Trace Route was sent, no response has been received." : { "localizations" : { @@ -2123,7 +2166,14 @@ } }, "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Жути отворени катанац означава да канал није безбедно шифрован, али се не користи за прецизне податке о локацији, при чему се или не користи кључ или се користи познати кључ од 1 бајта." + } + } + } }, "About" : { "localizations" : { @@ -2587,6 +2637,12 @@ "value" : "連絡先を追加" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додај контакт" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2609,6 +2665,12 @@ "value" : "Meshtasticノード%@を連絡先に追加" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додај Meshtastic чвор %@ као контакт" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2726,6 +2788,12 @@ "state" : "translated", "value" : "管理者キー" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Админ кључеви" + } } } }, @@ -2771,6 +2839,12 @@ "value" : "管理機能が有効" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Администрација активирана" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -2986,6 +3060,30 @@ } } } + }, + "sr" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Након %lld дана" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Након %lld дан" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Након %lld дана" + } + } + } + } } } }, @@ -4062,6 +4160,9 @@ } } } + }, + "App Notifications" : { + }, "App Settings" : { "localizations" : { @@ -4534,6 +4635,12 @@ "state" : "translated", "value" : "バックアップ" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Резервна копија" + } } } }, @@ -4544,6 +4651,12 @@ "state" : "translated", "value" : "プライベートキーをiCloudキーチェーンにバックアップします。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Направи резервну копију свог приватног кључа у iCloud Keychain." + } } } }, @@ -5176,6 +5289,12 @@ "state" : "translated", "value" : "BLE RSSI %lld" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE RSSI %lld" + } } } }, @@ -5805,6 +5924,12 @@ "value" : "この機能を有効にすることで、お客様のデバイスのリアルタイム地理位置が暗号化されずにMQTTプロトコル経由で送信されることを承知し、明示的に同意することを認めます。この位置データは、ライブマップ報告、デバイス追跡、関連テレメトリー機能などの目的で使用される場合があります。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућавањем ове функције, потврђујете и изричито пристајете на пренос географске локације вашег уређаја у реалном времену преко MQTT протокола без енкрипције. Ови подаци о локацији могу се користити за потребе као што су извештавање на мапи у реалном времену, праћење уређаја и сродне телеметријске функције." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -5885,6 +6010,12 @@ "state" : "translated", "value" : "使用バイト" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бајтова употребљено" + } } } }, @@ -7196,6 +7327,12 @@ "value" : "チャンネル使用率 %@%%" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Искоришћеност канала %@%" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7309,6 +7446,12 @@ "state" : "translated", "value" : "チャンネルヘルプ" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помоћ око канала" + } } } }, @@ -7535,6 +7678,12 @@ "state" : "translated", "value" : "古いノードをクリア" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очисти застареле чворове" + } } } }, @@ -7962,6 +8111,12 @@ "value" : "コミュニティサポート" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подршка заједница" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8105,6 +8260,12 @@ } } } + }, + "Configure Location Permissions" : { + + }, + "Configure notification permissions" : { + }, "Confirm" : { "localizations" : { @@ -8120,6 +8281,12 @@ "value" : "確認" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потврди" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8170,6 +8337,12 @@ "value" : "プロキシ経由でMQTTに接続" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повежи се на MQTT преко проксија" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8192,6 +8365,12 @@ "value" : "新しい無線機に接続しますか?" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повежи се на нови уређај?" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8312,6 +8491,12 @@ "value" : "接続済み無線機" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезани радио" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8398,6 +8583,12 @@ "value" : "新しい無線機に接続すると、電話上の全てのアプリデータがクリアされます。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повезивање са новим радиом ће обрисати све податке апликације на телефону." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8518,6 +8709,12 @@ "value" : "MQTT経由での暗号化されていないノードデータの共有に同意" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сагласност за дељење нешифрованих података чвора преко MQTT-а" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -8534,6 +8731,12 @@ "value" : "連絡先URL" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Урл контакт" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "needs_review", @@ -9015,6 +9218,9 @@ } } } + }, + "Critical Alerts" : { + }, "Current" : { "localizations" : { @@ -9172,6 +9378,12 @@ "value" : "現在、このノードでサポートされていない可能性のあるモジュールを表示しています。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренутно су приказани модули који можда нису подржани од стране овог чвора." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -9647,6 +9859,12 @@ "state" : "translated", "value" : "全ての設定、キー、BLEボンドを削除しますか?" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обрисати сву конфигурацију, кључеве и BLE повезнице?" + } } } }, @@ -9663,6 +9881,12 @@ "state" : "translated", "value" : "全ての設定を削除しますか?" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обрисати сву конфигурацију?" + } } } }, @@ -11150,6 +11374,12 @@ "state" : "translated", "value" : "ダイレクトメッセージキー" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Direct Message Key" + } } } }, @@ -11420,6 +11650,12 @@ "state" : "translated", "value" : "ノードを切断" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Раскачи чвор" + } } } }, @@ -11430,6 +11666,12 @@ "state" : "translated", "value" : "現在接続中のノードを切断します" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Раскачи тренутно повезани чвор" + } } } }, @@ -11867,6 +12109,12 @@ "value" : "完了" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Готово" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -12431,6 +12679,12 @@ "value" : "ローカルネットワーク上でUDP経由のパケットブロードキャストを有効にします。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогући емитовање пакета путем UDP-а преко локалне мреже.\n" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -12487,6 +12741,12 @@ "value" : "このデバイスをStore and Forwardサーバーとして有効にします。PSRAMを搭載したESP32デバイスが必要です。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогући овај уређај као Store and Forward сервер. Захтева ESP32 уређај са PSRAM меморијом." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -12705,6 +12965,12 @@ "value" : "Store and Forwardモジュールを有効にします。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућава Store and Forward модул.\n" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -12726,6 +12992,12 @@ "state" : "translated", "value" : "Ethernetを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућавање Ethernet-а ће онемогућити Bluetooth везу са апликацијом. TCP везе са чвором нису доступне на Apple уређајима.\n" + } } } }, @@ -12742,6 +13014,12 @@ "state" : "translated", "value" : "WiFiを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омогућавање WiFi-а ће онемогућити Bluetooth везу са апликацијом. TCP везе са чвором нису доступне на Apple уређајима.\n" + } } } }, @@ -13486,6 +13764,12 @@ "state" : "translated", "value" : "有効期限" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Истек рока" + } } } }, @@ -13834,6 +14118,12 @@ "state" : "translated", "value" : "工場出荷時リセットによりデバイスとアプリのデータが削除されます。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Фабричко ресетовање ће обрисати податке уређаја и апликације." + } } } }, @@ -13996,6 +14286,12 @@ "state" : "translated", "value" : "お気に入りと無視されたノードは常に保持されます。PKCキーを持たないノードは、ユーザーが設定したスケジュールでアプリデータベースからクリアされ、PKCキーを持つノードは間隔が7日以上に設定されている場合のみクリアされます。この機能は、デバイスノードデータベースに保存されていないノードのみをアプリから削除します。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Омиљени и игнорисани чворови увек се чувају. Чворови без PKC кључева се бришу из базе апликације према распореду који корисник подеси, док се чворови са PKC кључевима бришу само ако је интервал подешен на 7 дана или дуже. Ова функција брише из апликације само оне чворове који нису сачувани у бази података уређаја." + } } } }, @@ -15527,6 +15823,12 @@ "value" : "完全サポート" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Апсолутна подршка" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15542,6 +15844,12 @@ "state" : "translated", "value" : "現在使用中のプライベートキーを置き換える新しいプライベートキーを生成します。パブリックキーはプライベートキーから自動的に再生成されます。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Генериши нови приватни кључ који ће заменити тренутно коришћени. Јавни кључ ће аутоматски бити регенерисан из твог приватног кључа.\n" + } } } }, @@ -15616,6 +15924,12 @@ "state" : "translated", "value" : "パブリックキーから生成され、メッシュ上の他のノードに送信されて、共有秘密キーの計算を可能にします。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Генерисано из твог јавног кључа и послато другим чворовима на мрежи како би им омогућило да израчунају заједнички тајни кључ.\n" + } } } }, @@ -15720,6 +16034,9 @@ } } } + }, + "Get started" : { + }, "Get the latest alpha firmware" : { "localizations" : { @@ -15796,6 +16113,12 @@ "state" : "translated", "value" : "GitHubリポジトリ" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ГитХаб репозиторијум" + } } } }, @@ -16246,7 +16569,14 @@ } }, "Hard Reset" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тврдо ресетовање" + } + } + } }, "Hardware" : { "localizations" : { @@ -16557,6 +16887,12 @@ "state" : "translated", "value" : "サイドバーを隠す" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сакриј бочну траку" + } } } }, @@ -17388,6 +17724,12 @@ "value" : "上記を読み理解しました。MQTT経由でのノードデータの暗号化されない送信に自発的に同意します。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прочитао сам и разумем горе наведено. Добровољно дајем сагласност за нешифровану пренос мојих података чвора преко MQTT-а. Потврђујем да је Косово заувек Србија.\n" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -17812,6 +18154,12 @@ "value" : "Local Onlyのように外部メッシュからの観測メッセージを無視しますが、さらに進んで、ノードの既知リストにないノードからのメッセージも無視します。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнорише посматране поруке са страна мрежа као што је Local Only, али иде корак даље и игнорише поруке од чворова који већ нису у познатој листи чвора.\n" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -17828,6 +18176,12 @@ "value" : "オープンまたは復号化できない外部メッシュからの観測メッセージを無視します。ノードのローカル主要/副次チャンネルでのみメッセージを再ブロードキャストします。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнорише посматране поруке са страна мрежа које су отворене или које не може дешифровати. Поруке поново емитује само на локалним примарним/секундарним каналима чвора.\n" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -17871,7 +18225,14 @@ } }, "In addition to Config, Keys and BLE bonds will be wiped" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поред конфигурације, кључеви и BLE везе ће бити избрисани.\n" + } + } + } }, "Include" : { "localizations" : { @@ -18459,6 +18820,12 @@ "value" : "最新に移動" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скочи на тренутно" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -18514,6 +18881,12 @@ "state" : "translated", "value" : "キーバックアップ" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Резервна копија кључа" + } } } }, @@ -18694,6 +19067,12 @@ "state" : "translated", "value" : "緯度(度単位、例: 37.7749)" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Географска ширина у степенима (нпр. 42.6612)" + } } } }, @@ -18704,6 +19083,12 @@ "state" : "translated", "value" : "緯度は-90度から90度の間である必要があります" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Географска ширина мора бити између -90 и 90 степени.\n" + } } } }, @@ -19484,6 +19869,12 @@ "state" : "translated", "value" : "経度(度単位、例: -122.4194)" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Географска дужина у степенима (нпр. 20.2654)" + } } } }, @@ -19494,6 +19885,12 @@ "state" : "translated", "value" : "経度は-180度から180度の間である必要があります" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Географска дужина мора бити између -180 и 180 степени.\n" + } } } }, @@ -19626,7 +20023,14 @@ } }, "LoRa Config Changes:" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Промене LoRa конфигурације:" + } + } + } }, "LoRa config received: %@" : { "localizations" : { @@ -20407,6 +20811,9 @@ } } } + }, + "Meshtastic" : { + }, "Meshtastic Node %@ has shared channels with you" : { "localizations" : { @@ -20441,6 +20848,9 @@ } } } + }, + "Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings." : { + }, "Meshtastic® Copyright Meshtastic LLC" : { "localizations" : { @@ -20738,6 +21148,12 @@ "state" : "translated", "value" : "メッセージサイズ" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Величина поруке" + } } } }, @@ -20868,6 +21284,12 @@ "state" : "translated", "value" : "メッセージング" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дописивање" + } } } }, @@ -24313,6 +24735,12 @@ "value" : "SENSOR、TRACKER、TAK_TRACKERロールでのみ許可されており、CLIENT_MUTEロールと同様にすべての再ブロードキャストを抑制します。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дозвољено само за улоге SENSOR, TRACKER и TAK_TRACKER, ово ће онемогућити све поновне емитовања, слично као у улози CLIENT_MUTE." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -24329,6 +24757,12 @@ "value" : "コアポート番号からのパケットのみ再ブロードキャスト: ノード情報、テキスト、位置、テレメトリ、ルーティング。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поново емитује само пакете са основних портова: NodeInfo, Text, Position, Telemetry и Routing." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -25451,6 +25885,12 @@ "value" : "PAXカウンターログ" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log (лог фајл paxcounter-а)" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -25640,6 +26080,9 @@ } } } + }, + "Phone Location" : { + }, "Pin %lld" : { "localizations" : { @@ -25773,6 +26216,12 @@ "value" : "マップレポートは暗号化されていないため、あなたのデータが第三者によって永続的に保存・表示される可能性があることをご承知ください。Meshtasticは、このようなデータの保存、表示、開示について一切の責任を負いません。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Имајте на уму да, пошто извештај о мапи није шифрован, ваши подаци могу бити трајно сачувани и приказани од стране трећих лица. Meshtastic не преузима одговорност за такво чување, приказивање или откривање ових података." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26929,6 +27378,12 @@ "value" : "気圧" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Притисак" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27272,10 +27727,24 @@ } }, "Provide anonymous usage statistics and crash reports." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи анонимне статистике коришћења и извештаје о кваровима.\n" + } + } + } }, "Provide Confirmation" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пружи потврду\n" + } + } + } }, "Public Key" : { "localizations" : { @@ -27491,6 +27960,12 @@ "value" : "放射線" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зрачење" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27983,6 +28458,12 @@ "value" : "プライベートチャンネル上、または同じLoRaパラメータを持つ他のメッシュからの観測されたメッセージを再ブロードキャストします。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поново емитуј сваку посматрану поруку ако је била на нашем приватном каналу или од друге мреже са истим LoRa параметрима." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -28314,6 +28795,12 @@ "state" : "translated", "value" : "プライベートキーを再生成" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Регенериши приватни кључ" + } } } }, @@ -29058,6 +29545,12 @@ "state" : "translated", "value" : "復元" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обнови" + } } } }, @@ -30125,6 +30618,12 @@ "value" : "ALLと同じ動作ですが、パケットのデコードをスキップして単純に再ブロードキャストします。リピーター役割でのみ利用可能です。他の役割で設定するとALLの動作になります。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Исто понашање као код ALL, али прескаче дешифровање пакета и једноставно их поново емитује. Доступно само у улози репитера. Подешавање овог режима на било којој другој улози резултује понашањем као у режиму ALL." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30583,6 +31082,12 @@ "value" : "このQRコードをスキャンして、%@ を別のデバイスに追加してください。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скенирај овај QR код да додаш %@ на други уређај." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31067,6 +31572,12 @@ "value" : "ドロップダウンからノードを選択して、接続済みまたはリモートデバイスを管理してください。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одабери чвор из падајућег менија за управљање повезаним или удаљеним уређајима." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31339,6 +31850,12 @@ "value" : "サーバーの存在を通知するためのハートビートを送信します。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пошаљи heartbeat поруку да огласиш присуство сервера." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31614,6 +32131,9 @@ } } } + }, + "Send Notifications" : { + }, "Send Reboot OTA" : { "localizations" : { @@ -32590,6 +33110,12 @@ "value" : "サーバーオプション" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опција сервера" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -32725,6 +33251,12 @@ "state" : "translated", "value" : "現在の位置に設定" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Постави на тренутну локацију" + } } } }, @@ -32763,6 +33295,12 @@ "state" : "translated", "value" : "画面の時計表示を12時間形式に設定します。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поставља формат сата на екрану на 12-часовни." + } } } }, @@ -32902,6 +33440,12 @@ "value" : "連絡先QRを共有" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подели QR код контакта" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -33231,7 +33775,14 @@ } }, "Show a confirmation dialog before performing the factory reset" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прикажи дијалог за потврду пре извођења фабричког ресетовања." + } + } + } }, "Show alerts" : { "localizations" : { @@ -33438,7 +33989,14 @@ } }, "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приказује информације о LoRa радио уређају повезаном преко Bluetooth-а. Можете превући улево да бисте искључили радио, а дугим притиском покренути активан режим праћења." + } + } + } }, "Shut Down" : { "localizations" : { @@ -33950,6 +34508,12 @@ "value" : "土壌水分" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Влажност земљишта" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -33972,6 +34536,12 @@ "value" : "土壌温度" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Температура земљишта" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34117,6 +34687,12 @@ "state" : "translated", "value" : "アプリ開発をスポンサー" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Финансирај развој апликације" + } } } }, @@ -34548,6 +35124,12 @@ "value" : "蓄積転送サーバーには、PSRAM搭載のESP32デバイスまたはLinux Nativeが必要です。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Store and Forward сервери захтевају ESP32 уређај са PSRAM меморијом или Linux Native." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34852,6 +35434,12 @@ "value" : "Meshtastic連絡先URLを取得し、ノードデータベースに保存します" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прима Meshtastic контакт URL и чува га у базу података чворова" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -36072,6 +36660,12 @@ "value" : "ルーター役割は山頂や塔のような見晴らしの良い高所での使用を想定して設計されています。このノードは、ネットワーク内の大部分のノードと良好な直接接続を保持できる必要があります。そうでなければ、ネットワークに深刻な影響を与えることになります。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рутер улоге су намењене локацијама са високим положајем као што су врхови планина и куле. Овај чвор мора имати добру директну везу са већином чворова у мрежи, иначе ће то значајно оштетити мрежу." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -36252,6 +36846,12 @@ "value" : "追加するノードのURL" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL чвора који треба додати" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -36261,7 +36861,14 @@ } }, "There has been no response to a request for device metadata via PKC admin for this node." : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Није било одговора на захтев за метаподатке уређаја преко PKC админ-а за овај чвор." + } + } + } }, "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" : { @@ -36621,6 +37228,12 @@ "value" : "このノードは設定可能なモジュールをサポートしていません。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Овај чвор не подржава ниједан модул који се може конфигурисати." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -37281,6 +37894,12 @@ "value" : "CCPAやGDPRなどのプライバシー法に準拠するため、正確な位置データの共有は避けています。代わりに、あなたのプライバシーを保護するために匿名化または近似(不正確)の位置情報を使用します。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Да бисмо поштовали законе о приватности као што су CCPA и GDPR, избегавамо дељење тачних података о локацији. Уместо тога, користимо анонимизоване или приближне (непрецизне) информације о локацији како бисмо заштитили вашу приватност." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -37421,6 +38040,12 @@ "value" : "トレースルート(%@秒)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Праћење трасе (за %@ секунди)" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -38215,6 +38840,12 @@ "value" : "UDPブロードキャスト" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDP емитовање" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -38499,7 +39130,7 @@ }, "sr" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "непознато" } }, @@ -38640,6 +39271,12 @@ "state" : "translated", "value" : "メッセージ不可" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немогуће послати поруку" + } } } }, @@ -38650,6 +39287,12 @@ "state" : "translated", "value" : "監視なし" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ненадгледано" + } } } }, @@ -39194,7 +39837,14 @@ } }, "Usage and Crash Data" : { - + "localizations" : { + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подaци о коришћењу и падовима апликације" + } + } + } }, "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" : { @@ -39265,6 +39915,12 @@ "state" : "translated", "value" : "自分の位置を使用" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи моју локацију" + } } } }, @@ -39371,6 +40027,12 @@ "state" : "translated", "value" : "監視されていないまたはインフラストラクチャノードを識別するために使用され、応答しないノードにはメッセージング機能が利用できないようにします。" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Користи се за идентификацију ненадгледаних или инфраструктурних чворова како би се онемогућила комуникација са чворовима који никада неће одговорити." + } } } }, @@ -39822,6 +40484,12 @@ "value" : "バージョン: %@ (%@)" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Верзија: %1$@ (%2$@)" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -40050,6 +40718,12 @@ "value" : "Volts %@" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Волти %@" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -40265,6 +40939,12 @@ "state" : "translated", "value" : "ウェイポイントの送信に失敗" } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Слање тачке пута није успело" + } } } }, @@ -40482,6 +41162,12 @@ "value" : "重量" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тежина" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -40489,6 +41175,9 @@ } } } + }, + "Welcome to" : { + }, "What does the lock mean?" : { "localizations" : { @@ -40846,6 +41535,12 @@ "value" : "風" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Јебиветар" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -41389,6 +42084,12 @@ "value" : "ノードは設定されたMQTTサーバーに定期的に暗号化されていないマップレポートパケットを送信します。これにはID、短縮名と長い名前、おおよその位置、ハードウェアモデル、役割、ファームウェアバージョン、LoRa地域、モデムプリセット、プライマリチャンネル名が含まれます。" } }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Твој чвор ће периодично слати нешифровани извештај о мапи на подешени MQTT сервер. Извештај укључује ID, кратко и дуго име, приближну локацију, модел хардвера, улогу, верзију фирмвера, LoRa регион, модемске поставке и име примарног канала." + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 8e1c0929..828dcafb 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EB02E172F41003D191E /* DatadogRUM */; }; 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */; }; 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; }; + 10D109F22E2047D600536CE6 /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F12E2047D600536CE6 /* DatadogSessionReplay */; }; + 10D109F42E2047D600536CE6 /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F32E2047D600536CE6 /* DatadogTrace */; }; 231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; }; 231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; }; 231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */; }; @@ -145,6 +147,7 @@ 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 */; }; + DD74ED0D2DC6A0C90059AC10 /* DeviceOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */; }; @@ -200,7 +203,6 @@ DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; }; - DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; @@ -445,6 +447,7 @@ 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 = ""; }; + DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOnboarding.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 = ""; }; @@ -514,7 +517,6 @@ DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = ""; }; DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; @@ -599,9 +601,11 @@ 102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */, 25A978BA2C13F8ED0003AAE7 /* MeshtasticProtobufs in Frameworks */, 102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */, + 10D109F22E2047D600536CE6 /* DatadogSessionReplay in Frameworks */, 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */, 102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */, DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */, + 10D109F42E2047D600536CE6 /* DatadogTrace in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -871,6 +875,14 @@ path = Help; sourceTree = ""; }; + DD74ED0B2DC6A0900059AC10 /* Onboarding */ = { + isa = PBXGroup; + children = ( + DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( @@ -1010,11 +1022,12 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( - DD6D5A312CA1176A00ED3032 /* Layouts */, - DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, + DDC2E18D26CE25CB0042C5E4 /* Helpers */, + DD6D5A312CA1176A00ED3032 /* Layouts */, DDC2E18B26CE25A70042C5E4 /* Messages */, DD47E3CA26F0E50300029299 /* Nodes */, + DD74ED0B2DC6A0900059AC10 /* Onboarding */, DD4A911C2708C57100501B7E /* Settings */, DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */, ); @@ -1090,7 +1103,6 @@ DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, - DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, @@ -1248,6 +1260,8 @@ 102B5EAC2E172F41003D191E /* DatadogCrashReporting */, 102B5EAE2E172F41003D191E /* DatadogLogs */, 102B5EB02E172F41003D191E /* DatadogRUM */, + 10D109F12E2047D600536CE6 /* DatadogSessionReplay */, + 10D109F32E2047D600536CE6 /* DatadogTrace */, ); productName = MeshtasticClient; productReference = DDC2E15426CE248E0042C5E4 /* Meshtastic.app */; @@ -1419,7 +1433,6 @@ 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, 231B3F272D0885240069A07D /* MetricsColumnDetail.swift in Sources */, - DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */, 6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, @@ -1468,6 +1481,7 @@ DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, + DD74ED0D2DC6A0C90059AC10 /* DeviceOnboarding.swift in Sources */, DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */, B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, @@ -1842,7 +1856,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.11; + MARKETING_VERSION = 2.6.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1875,7 +1889,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.11; + MARKETING_VERSION = 2.6.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1906,7 +1920,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.11; + MARKETING_VERSION = 2.6.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1938,7 +1952,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.11; + MARKETING_VERSION = 2.6.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2046,6 +2060,16 @@ package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; productName = DatadogRUM; }; + 10D109F12E2047D600536CE6 /* DatadogSessionReplay */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogSessionReplay; + }; + 10D109F32E2047D600536CE6 /* DatadogTrace */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogTrace; + }; 25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */ = { isa = XCSwiftPackageProductDependency; productName = MeshtasticProtobufs; diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 68276229..4a0652bc 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0dabe052e9e56f8514254d01df9aa7245e16b28a649d59bac6781d4ac9a79efa", + "originHash" : "fd71b247ba909b0eb360db5530e1068363839c5e169dea6f6a9974b2d98276f4", "pins" : [ { "identity" : "cocoamqtt", diff --git a/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json new file mode 100644 index 00000000..5f4c592b --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wio_tracker_l1_case.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/wio_tracker_l1_case.svg b/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/wio_tracker_l1_case.svg new file mode 100644 index 00000000..5104c74e --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDWIOTRACKERL1.imageset/wio_tracker_l1_case.svg @@ -0,0 +1,710 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 8355c4b4..7be3a1ae 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -78,7 +78,7 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable { var description: String { switch self { case .none: - return "None".localized + return "map.usertrackingmode.none".localized case .follow: return "Follow".localized case .followWithHeading: diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index c6dc7628..b196a7a7 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -17,6 +17,7 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case cn = 4 case jp = 5 case anz = 6 + case anz433 = 22 case kr = 7 case tw = 8 case ru = 9 @@ -31,6 +32,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case ph433 = 19 case ph868 = 20 case ph915 = 21 + case kz433 = 23 + case kz863 = 24 case lora24 = 13 var topic: String { switch self { @@ -48,6 +51,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { "JP" case .anz: "ANZ" + case .anz433: + "ANZ_433" case .kr: "KR" case .tw: @@ -76,6 +81,10 @@ enum RegionCodes: Int, CaseIterable, Identifiable { "ph_868" case .ph915: "ph_915" + case .kz433: + "KZ_433" + case .kz863: + "KZ_863" case .lora24: "LORA_24" } } @@ -96,6 +105,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Japan".localized case .anz: return "Australia / New Zealand".localized + case .anz433: + return "Australia / New Zealand 433MHz".localized case .kr: return "Korea".localized case .tw: @@ -112,8 +123,6 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Ukraine 433MHz".localized case .ua868: return "Ukraine 868MHz".localized - case .lora24: - return "2.4 Ghz".localized case .my433: return "Malaysia 433MHz".localized case .my919: @@ -126,6 +135,12 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Philippines 868MHz".localized case .ph915: return "Philippines 915MHz".localized + case .kz433: + return "Kazakhstan 433MHz".localized + case .kz863: + return "Kazakhstan 863MHz".localized + case .lora24: + return "2.4 Ghz".localized } } var dutyCycle: Int { @@ -174,6 +189,12 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return 100 case .ph915: return 100 + case .anz433: + return 100 + case .kz433: + return 100 + case .kz863: + return 100 } } var isCountry: Bool { @@ -222,6 +243,12 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return true case .ph915: return true + case .anz433: + return false + case .kz433: + return true + case .kz863: + return true } } func protoEnumValue() -> Config.LoRaConfig.RegionCode { @@ -271,6 +298,12 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return Config.LoRaConfig.RegionCode.ph868 case .ph915: return Config.LoRaConfig.RegionCode.ph915 + case .anz433: + return Config.LoRaConfig.RegionCode.anz433 + case .kz433: + return Config.LoRaConfig.RegionCode.kz433 + case .kz863: + return Config.LoRaConfig.RegionCode.kz863 } } } diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 14bc4948..f6f0b206 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -84,6 +84,10 @@ extension UserEntity { return "SEEEDXIAOS3" case "WIOWM1110": return "WIOWM1110" + case "SEEEDSOLARNODE": + return "SEEEDSOLARNODE" + case "SEEEDWIOTRACKERL1": + return "SEEEDWIOTRACKERL1" /// RAK Wireless case "RAK4631": return "RAK4631" diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 0ca337e9..0b124ac5 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -74,6 +74,8 @@ extension UserDefaults { case environmentEnableWeatherKit case enableAdministration case mapReportingOptIn + case firstLaunch + case showDeviceOnboarding case usageDataAndCrashReporting case testIntEnum } @@ -159,6 +161,11 @@ extension UserDefaults { @UserDefault(.usageDataAndCrashReporting, defaultValue: true) static var usageDataAndCrashReporting: Bool + @UserDefault(.firstLaunch, defaultValue: true) + static var firstLaunch: Bool + + @UserDefault(.showDeviceOnboarding, defaultValue: false) + static var showDeviceOnboarding: Bool @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 720ff558..cc290f39 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -60,9 +60,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let NONCE_ONLY_DB = 69421 private var isWaitingForWantConfigResponse = false - private var wantConfigTimer: Timer? - private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 6 + private var wantConfigTimer: Timer? + private var wantConfigRetryCount = 0 + private let maxWantConfigRetries = 6 private let wantConfigTimeoutInterval: TimeInterval = 6.0 // MARK: init @@ -799,33 +799,42 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } } + guard let cp = connectedPeripheral else { + return + } // Channels - if decodedInfo.channel.isInitialized && connectedPeripheral != nil { + if decodedInfo.channel.isInitialized { nowKnown = true - channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context) + channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: cp.num), context: context) } // Config - if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil && self.connectedPeripheral?.num != 0 { + if decodedInfo.config.isInitialized && !invalidVersion && cp.num != 0 { nowKnown = true - localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: cp.num), nodeLongName: cp.longName) } // Module Config - if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { + if decodedInfo.moduleConfig.isInitialized && !invalidVersion && cp.num != 0 { onWantConfigResponseReceived() nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: cp.num), nodeLongName: cp.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - if let connectedNum = self.connectedPeripheral?.num, connectedNum > 0 { - _ = self.getCannedMessageModuleMessages(destNum: connectedNum, wantResponse: true) - } + _ = self.getCannedMessageModuleMessages(destNum: cp.num, wantResponse: true) + + } + } + if decodedInfo.config.payloadVariant == Config.OneOf_PayloadVariant.device(decodedInfo.config.device) { + var dc = decodedInfo.config.device + if dc.tzdef.isEmpty { + dc.tzdef = TimeZone.current.posixDescription + _ = self.saveTimeZone(config: dc, user: cp.num) } } } // Device Metadata if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion { nowKnown = true - deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context) + deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: cp.num, context: context) connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".") if lastDotIndex == nil { @@ -1084,6 +1093,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .keyVerificationApp: Logger.mesh.warning("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + case .cayenneApp: + Logger.mesh.info("🕸️ MESH PACKET received Cayenne App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_CONFIG { @@ -1092,6 +1103,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate isSubscribed = true allowDisconnect = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") + if UserDefaults.firstLaunch { + UserDefaults.showDeviceOnboarding = true + } if sendTime() { } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) @@ -2231,6 +2245,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return 0 } + public func saveTimeZone(config: Config.DeviceConfig, user: Int64) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setConfig.device = config + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(user) + meshPacket.from = UInt32(user) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift deleted file mode 100644 index 978ae5a8..00000000 --- a/Meshtastic/Helpers/LocationHelper.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import CoreLocation -import MapKit -import OSLog - -class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { - static let shared = LocationHelper() - var locationManager = CLLocationManager() - - // @Published var region = MKCoordinateRegion() - @Published var authorizationStatus: CLAuthorizationStatus? - override init() { - super.init() - locationManager.delegate = self - locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - locationManager.pausesLocationUpdatesAutomatically = true - locationManager.allowsBackgroundLocationUpdates = true - locationManager.activityType = .other - } - // Apple Park - static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) - static var currentLocation: CLLocationCoordinate2D { - guard let location = shared.locationManager.location else { - return DefaultLocation - } - return location.coordinate - } - static var satsInView: Int { - // If we have a position we have a sat - var sats = 1 - if shared.locationManager.location?.verticalAccuracy ?? 0 > 0 { - sats = 4 - if 0...5 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 12 - } else if 6...15 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 10 - } else if 16...30 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 9 - } else if 31...45 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 7 - } else if 46...60 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 5 - } - } else if shared.locationManager.location?.verticalAccuracy ?? 0 < 0 && 60...300 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 3 - } else if shared.locationManager.location?.verticalAccuracy ?? 0 < 0 && shared.locationManager.location?.horizontalAccuracy ?? 0 > 300 { - sats = 2 - } - return sats - } - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - switch manager.authorizationStatus { - case .authorizedAlways: - authorizationStatus = .authorizedAlways - case .authorizedWhenInUse: - authorizationStatus = .authorizedWhenInUse - locationManager.requestLocation() - case .restricted: - authorizationStatus = .restricted - case .denied: - authorizationStatus = .denied - case .notDetermined: - authorizationStatus = .notDetermined - locationManager.requestAlwaysAuthorization() - default: - break - } - } - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - - } - func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - Logger.services.error("Location manager error: \(error.localizedDescription, privacy: .public)") - } -} diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 23493604..645959f3 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -10,14 +10,14 @@ import CoreLocation import OSLog // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. -@MainActor class LocationsHandler: ObservableObject { +@MainActor class LocationsHandler: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate { static let shared = LocationsHandler() // Create a single, shared instance of the object. - private let manager: CLLocationManager + public var manager = CLLocationManager() private var background: CLBackgroundActivitySession? var enableSmartPosition: Bool = UserDefaults.enableSmartPosition - @Published var locationsArray: [CLLocation] + @Published var locationsArray: [CLLocation] = [CLLocation]() @Published var isStationary = false @Published var count = 0 @Published var isRecording = false @@ -38,16 +38,29 @@ import OSLog UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted") } } + // The continuation we will use to asynchronously ask the user permission to track their location. + private var permissionContinuation: CheckedContinuation? + func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { + return await withCheckedContinuation { continuation in + self.permissionContinuation = continuation + manager.requestAlwaysAuthorization() + } + } + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + // This is the line you need to add + permissionContinuation?.resume(returning: manager.authorizationStatus) + } - private init() { - self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. + override init() { + super.init() + self.manager.delegate = self self.manager.allowsBackgroundLocationUpdates = true - locationsArray = [CLLocation]() } func startLocationUpdates() { - if self.manager.authorizationStatus == .notDetermined { - self.manager.requestWhenInUseAuthorization() + let status = self.manager.authorizationStatus + guard status == .authorizedAlways || status == .authorizedWhenInUse else { + return } Logger.services.info("📍 [App] Starting location updates") Task { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 10a3d69b..24a577b2 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1027,7 +1027,7 @@ 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.isEmoji ? newMessage.replyID : newMessage.messageId)", messageId: newMessage.messageId, channel: newMessage.channel, userNum: Int64(packet.from), @@ -1058,7 +1058,7 @@ 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.isEmoji ? newMessage.replyID : newMessage.messageId)", messageId: newMessage.messageId, channel: newMessage.channel, userNum: Int64(newMessage.fromUser?.userId ?? "0"), diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 9ce07142..d4de2bf7 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -8,6 +8,8 @@ import MeshtasticProtobufs import DatadogCore import DatadogCrashReporting import DatadogRUM +import DatadogTrace +import DatadogLogs @main struct MeshtasticAppleApp: App { @@ -41,14 +43,24 @@ struct MeshtasticAppleApp: App { env: environment, site: .us5 ), - trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted + trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted, + ) + DatadogCrashReporting.CrashReporting.enable() + + Logs.enable() + + Trace.enable( + with: Trace.Configuration( + sampleRate: 100, networkInfoEnabled: true // 100% sampling for development/testing, reduce for production + ) ) RUM.enable( with: RUM.Configuration( applicationID: appID, uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(), - uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate() + uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate(), + trackBackgroundEvents: true ) ) self._appState = ObservedObject(wrappedValue: appState) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index bc0093a8..63f9efbc 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,19 +8,6 @@ import CoreData import MeshtasticProtobufs import OSLog -// MARK: - Safe Conversion Helpers -private func safeInt32(from value: UInt32) -> Int32 { - return Int32(clamping: value) -} - -private func safeInt32(from value: Int) -> Int32 { - return Int32(clamping: value) -} - -private func safeInt32(from value: UInt64) -> Int32 { - return Int32(clamping: value) -} - public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { var nodeExpireTime: TimeInterval { return TimeInterval(-nodeExpireDays * 86400) @@ -807,32 +794,32 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, s let newPositionConfig = PositionConfigEntity(context: context) newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.gpsEnabled - newPositionConfig.gpsMode = Int32(config.gpsMode.rawValue) - newPositionConfig.rxGpio = Int32(config.rxGpio) - newPositionConfig.txGpio = Int32(config.txGpio) - newPositionConfig.gpsEnGpio = Int32(config.gpsEnGpio) + newPositionConfig.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) + newPositionConfig.rxGpio = Int32(truncatingIfNeeded: config.rxGpio) + newPositionConfig.txGpio = Int32(truncatingIfNeeded: config.txGpio) + newPositionConfig.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio) newPositionConfig.fixedPosition = config.fixedPosition newPositionConfig.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) - newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) - newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) - newPositionConfig.positionFlags = Int32(config.positionFlags) + newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs) + newPositionConfig.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance) + newPositionConfig.positionFlags = Int32(truncatingIfNeeded: config.positionFlags) newPositionConfig.gpsAttemptTime = 900 - newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval) + newPositionConfig.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval) fetchedNode[0].positionConfig = newPositionConfig } else { fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled - fetchedNode[0].positionConfig?.gpsMode = Int32(config.gpsMode.rawValue) - fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio) - fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) - fetchedNode[0].positionConfig?.gpsEnGpio = Int32(config.gpsEnGpio) + fetchedNode[0].positionConfig?.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) + fetchedNode[0].positionConfig?.rxGpio = Int32(truncatingIfNeeded: config.rxGpio) + fetchedNode[0].positionConfig?.txGpio = Int32(truncatingIfNeeded: config.txGpio) + fetchedNode[0].positionConfig?.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio) fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) - fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) - fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) + fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance) fetchedNode[0].positionConfig?.gpsAttemptTime = 900 - fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) - fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) + fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval) + fetchedNode[0].positionConfig?.positionFlags = Int32(truncatingIfNeeded: config.positionFlags) } if sessionPasskey != nil { fetchedNode[0].sessionPasskey = sessionPasskey @@ -937,6 +924,8 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] if config.adminKey.count > 1 { fetchedNode[0].securityConfig?.adminKey2 = config.adminKey[1] + } + if config.adminKey.count > 2 { fetchedNode[0].securityConfig?.adminKey3 = config.adminKey[2] } } @@ -1512,23 +1501,23 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod if !fetchedNode.isEmpty { if fetchedNode[0].telemetryConfig == nil { let newTelemetryConfig = TelemetryConfigEntity(context: context) - newTelemetryConfig.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) - newTelemetryConfig.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) + newTelemetryConfig.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) + newTelemetryConfig.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled - newTelemetryConfig.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) + newTelemetryConfig.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval) newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig } else { - fetchedNode[0].telemetryConfig?.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) - fetchedNode[0].telemetryConfig?.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) + fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) + fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled - fetchedNode[0].telemetryConfig?.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) + fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval) fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } if sessionPasskey != nil { diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 4163ebaa..e2cfca54 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -923,6 +923,20 @@ ], "requiresDfu": true }, + { + "hwModel": 100, + "hwModelSlug": "SEEED_WIO_TRACKER_L1_EINK", + "platformioTarget": "seeed_wio_tracker_L1_eink", + "architecture": "nrf52840", + "activelySupported": false, + "supportLevel": 1, + "displayName": "Seeed Wio Tracker L1 E-Ink", + "tags": [ + "Seeed" + ], + "requiresDfu": true, + "hasInkHud": true + }, { "hwModel": 97, "hwModelSlug": "CROWPANEL", diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 53e27ae6..284eba62 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,20 +27,6 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" - init () { - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.getNotificationSettings(completionHandler: { (settings) in - if settings.authorizationStatus == .notDetermined { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .criticalAlert]) { success, error in - if success { - Logger.services.info("Notifications are all set!") - } else if let error = error { - Logger.services.error("\(error.localizedDescription, privacy: .public)") - } - } - } - }) - } var body: some View { NavigationStack { VStack { diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 9eb1ce56..05f227b5 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -5,11 +5,10 @@ import SwiftUI struct ContentView: View { - @ObservedObject - var appState: AppState + @ObservedObject var appState: AppState - @ObservedObject - var router: Router + @ObservedObject var router: Router + @State var isShowingDeviceOnboardingFlow: Bool = false init(appState: AppState, router: Router) { self.appState = appState @@ -58,6 +57,21 @@ struct ContentView: View { .font(.title) } .tag(NavigationState.Tab.settings) + }.sheet( + isPresented: $isShowingDeviceOnboardingFlow, + onDismiss: { + UserDefaults.firstLaunch = false + }, content: { + DeviceOnboarding() + } + ) + .onAppear { + if UserDefaults.firstLaunch { + isShowingDeviceOnboardingFlow = true + } + } + .onChange(of: UserDefaults.showDeviceOnboarding) { newValue in + isShowingDeviceOnboardingFlow = newValue } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 21fd6fa5..82522223 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -284,6 +284,9 @@ struct NodeList: View { // Make sure the ZStack passes through accessibility to the ConnectedDevice component .accessibilityElement(children: .contain) ) + .onDisappear { + router.navigationState.nodeListSelectedNodeNum = nil + } } } else { ContentUnavailableView("Select Node", systemImage: "flipphone") diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift new file mode 100644 index 00000000..33e6e3fa --- /dev/null +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -0,0 +1,284 @@ +import CoreBluetooth +import OSLog +import SwiftUI +import Foundation +import MapKit + +struct DeviceOnboarding: View { + enum SetupGuide: Hashable { + case notifications + case location + } + + @EnvironmentObject var bleManager: BLEManager + @State var navigationPath: [SetupGuide] = [] + @State var locationStatus = LocationsHandler.shared.manager.authorizationStatus + + @Environment(\.dismiss) var dismiss + + /// The Title View + var title: some View { + VStack { + Text("Welcome to") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + Text("Meshtastic") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + } + + var welcomeView: some View { + VStack { + ScrollView(.vertical, showsIndicators: false) { + VStack { + // Title + title + .padding(.top) + // Onboarding + VStack(alignment: .leading, spacing: 16) { + makeRow( + icon: "antenna.radiowaves.left.and.right", + title: "Stay Connected Anywhere", + subtitle: "Communicate off-the-grid with your friends and community without cell service." + ) + + makeRow( + icon: "point.3.connected.trianglepath.dotted", + title: "Create Your Own Networks", + subtitle: "Easily set up private mesh networks for secure and reliable communication in remote areas." + ) + + makeRow( + icon: "location", + title: "Track and Share Locations", + subtitle: "Share your location in real-time and keep your group coordinated with integrated GPS features." + ) + } + .padding() + } + .interactiveDismissDisabled() + } + Spacer() + if bleManager.isSwitchedOn { + Button { + Task { + await goToNextStep(after: nil) + } + } label: { + Text("Get started") + .frame(maxWidth: .infinity) + } + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + } + + var notificationView: some View { + VStack { + VStack { + Text("App Notifications") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() + VStack(alignment: .leading, spacing: 16) { + Text("Send Notifications") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "message", + title: "Incoming Messages", + subtitle: "Meshtastic notifications for channel messages and direct messages" + ) + makeRow( + icon: "flipphone", + title: "New Nodes", + subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." + ) + makeRow( + icon: "battery.25percent", + title: "Low Battery", + subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." + ) + Text("Critical Alerts") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "exclamationmark.triangle.fill", + subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center." + ) + } + .padding() + Spacer() + Button { + Task { + await requestNotificationsPermissions() + await goToNextStep(after: .notifications) + } + } label: { + Text("Configure notification permissions") + .frame(maxWidth: .infinity) + } + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + + var locationView: some View { + VStack { + VStack { + Text("Phone Location") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + VStack(alignment: .leading, spacing: 16) { + Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.") + .font(.body.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "location", + title: "Share Location", + subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node." + ) + makeRow( + icon: "lines.measurement.horizontal", + title: "Distance Measurements", + subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available." + ) + makeRow( + icon: "line.3.horizontal.decrease.circle", + title: "Distance Filters", + subtitle: "Filter the node list and mesh map based on proximity to your phone." + ) + makeRow( + icon: "mappin", + title: "Mesh Map Location", + subtitle: "Enables the blue location dot for your phone in the mesh map." + ) + } + .padding() + Spacer() + Button { + Task { + await requestLocationPermissions() + } + } label: { + Text("Configure Location Permissions") + .frame(maxWidth: .infinity) + } + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + + var body: some View { + NavigationStack(path: $navigationPath) { + welcomeView + .navigationDestination(for: SetupGuide.self) { guide in + switch guide { + case .notifications: + notificationView + case .location: + locationView + } + } + } + .toolbar(.hidden) + } + + @ViewBuilder + func makeRow( + icon: String, + title: String = "", + subtitle: String + ) -> some View { + HStack(alignment: .center) { + Image(systemName: icon) + .resizable() + .symbolRenderingMode(.multicolor) + .font(.subheadline) + .aspectRatio(contentMode: .fit) + .padding() + .frame(width: 72, height: 72) + + VStack(alignment: .leading) { + Text(title) + .font(.subheadline.weight(.semibold)) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + + Text(subtitle) + .font(.subheadline) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + }.multilineTextAlignment(.leading) + }.accessibilityElement(children: .combine) + } + + // MARK: Navigation + func goToNextStep(after step: SetupGuide?) async { + switch step { + case .none: + let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus + let criticalAlert = await UNUserNotificationCenter.current().notificationSettings().criticalAlertSetting + if status == .notDetermined && criticalAlert == .notSupported { + navigationPath.append(.notifications) + } else { + fallthrough + } + case .notifications: + locationStatus = LocationsHandler.shared.manager.authorizationStatus + if locationStatus == .notDetermined || locationStatus == .restricted || locationStatus == .denied { + navigationPath.append(.location) + } else { + fallthrough + } + case .location: + let status = LocationsHandler.shared.manager.authorizationStatus + if status != .notDetermined && status != .restricted && status != .denied { + dismiss() + } + } + } + + // MARK: Permission Checks + func requestNotificationsPermissions() async { + let center = UNUserNotificationCenter.current() + do { + let success = try await center.requestAuthorization(options: [.alert, .badge, .sound, .criticalAlert]) + if success { + Logger.services.info("Notification permissions are enabled") + } else { + Logger.services.info("Notification permissions denied") + } + } catch { + Logger.services.error("Notification permissions error: \(error.localizedDescription)") + } + } + + func requestLocationPermissions() async { + locationStatus = await LocationsHandler.shared.requestLocationAlwaysPermissions() + if locationStatus != .notDetermined { + Logger.services.info("Location permissions are enabled") + } else { + Logger.services.info("Location permissions denied") + } + dismiss() + } +} diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 834dcc0f..3085192d 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -334,11 +334,5 @@ struct DeviceConfig: View { self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? false self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true self.tzdef = node?.deviceConfig?.tzdef ?? "" - if self.tzdef.isEmpty { - self.tzdef = TimeZone.current.posixDescription - self.hasChanges = true - } else { - self.hasChanges = false - } } } diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index e37bc908..63797080 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -436,6 +436,12 @@ public struct Config: Sendable { /// Non-notification system buzzer tones only. /// Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. case systemOnly // = 3 + + /// + /// Direct Message notifications only. + /// Buzzer is enabled only for direct messages and alerts, but not for button presses. + /// External notification config determines the specifics of the notification behavior. + case directMsgOnly // = 4 case UNRECOGNIZED(Int) public init() { @@ -448,6 +454,7 @@ public struct Config: Sendable { case 1: self = .disabled case 2: self = .notificationsOnly case 3: self = .systemOnly + case 4: self = .directMsgOnly default: self = .UNRECOGNIZED(rawValue) } } @@ -458,6 +465,7 @@ public struct Config: Sendable { case .disabled: return 1 case .notificationsOnly: return 2 case .systemOnly: return 3 + case .directMsgOnly: return 4 case .UNRECOGNIZED(let i): return i } } @@ -468,6 +476,7 @@ public struct Config: Sendable { .disabled, .notificationsOnly, .systemOnly, + .directMsgOnly, ] } @@ -1579,6 +1588,18 @@ public struct Config: Sendable { /// /// Philippines 915mhz case ph915 // = 21 + + /// + /// Australia / New Zealand 433MHz + case anz433 // = 22 + + /// + /// Kazakhstan 433MHz + case kz433 // = 23 + + /// + /// Kazakhstan 863MHz + case kz863 // = 24 case UNRECOGNIZED(Int) public init() { @@ -1609,6 +1630,9 @@ public struct Config: Sendable { case 19: self = .ph433 case 20: self = .ph868 case 21: self = .ph915 + case 22: self = .anz433 + case 23: self = .kz433 + case 24: self = .kz863 default: self = .UNRECOGNIZED(rawValue) } } @@ -1637,6 +1661,9 @@ public struct Config: Sendable { case .ph433: return 19 case .ph868: return 20 case .ph915: return 21 + case .anz433: return 22 + case .kz433: return 23 + case .kz863: return 24 case .UNRECOGNIZED(let i): return i } } @@ -1665,6 +1692,9 @@ public struct Config: Sendable { .ph433, .ph868, .ph915, + .anz433, + .kz433, + .kz863, ] } @@ -2247,6 +2277,7 @@ extension Config.DeviceConfig.BuzzerMode: SwiftProtobuf._ProtoNameProviding { 1: .same(proto: "DISABLED"), 2: .same(proto: "NOTIFICATIONS_ONLY"), 3: .same(proto: "SYSTEM_ONLY"), + 4: .same(proto: "DIRECT_MSG_ONLY"), ] } @@ -2992,6 +3023,9 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { 19: .same(proto: "PH_433"), 20: .same(proto: "PH_868"), 21: .same(proto: "PH_915"), + 22: .same(proto: "ANZ_433"), + 23: .same(proto: "KZ_433"), + 24: .same(proto: "KZ_863"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift index 9607abe1..d5a3560e 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -21,6 +21,53 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +public enum CompassMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// Compass with dynamic ring and heading + case dynamic // = 0 + + /// + /// Compass with fixed ring and heading + case fixedRing // = 1 + + /// + /// Compass with heading and freeze option + case freezeHeading // = 2 + case UNRECOGNIZED(Int) + + public init() { + self = .dynamic + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .dynamic + case 1: self = .fixedRing + case 2: self = .freezeHeading + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .dynamic: return 0 + case .fixedRing: return 1 + case .freezeHeading: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [CompassMode] = [ + .dynamic, + .fixedRing, + .freezeHeading, + ] + +} + public enum Theme: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int @@ -350,6 +397,29 @@ public struct DeviceUIConfig: @unchecked Sendable { /// Clears the value of `mapData`. Subsequent reads from it will return its default value. public mutating func clearMapData() {_uniqueStorage()._mapData = nil} + /// + /// Compass mode + public var compassMode: CompassMode { + get {return _storage._compassMode} + set {_uniqueStorage()._compassMode = newValue} + } + + /// + /// RGB color for BaseUI + /// 0xRRGGBB format, e.g. 0xFF0000 for red + public var screenRgbColor: UInt32 { + get {return _storage._screenRgbColor} + set {_uniqueStorage()._screenRgbColor = newValue} + } + + /// + /// Clockface analog style + /// true for analog clockface, false for digital clockface + public var isClockfaceAnalog: Bool { + get {return _storage._isClockfaceAnalog} + set {_uniqueStorage()._isClockfaceAnalog = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -482,6 +552,14 @@ public struct Map: Sendable { fileprivate let _protobuf_package = "meshtastic" +extension CompassMode: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DYNAMIC"), + 1: .same(proto: "FIXED_RING"), + 2: .same(proto: "FREEZE_HEADING"), + ] +} + extension Theme: SwiftProtobuf._ProtoNameProviding { public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 0: .same(proto: "DARK"), @@ -533,6 +611,9 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 13: .standard(proto: "node_highlight"), 14: .standard(proto: "calibration_data"), 15: .standard(proto: "map_data"), + 16: .standard(proto: "compass_mode"), + 17: .standard(proto: "screen_rgb_color"), + 18: .standard(proto: "is_clockface_analog"), ] fileprivate class _StorageClass { @@ -551,6 +632,9 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement var _nodeHighlight: NodeHighlight? = nil var _calibrationData: Data = Data() var _mapData: Map? = nil + var _compassMode: CompassMode = .dynamic + var _screenRgbColor: UInt32 = 0 + var _isClockfaceAnalog: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -580,6 +664,9 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement _nodeHighlight = source._nodeHighlight _calibrationData = source._calibrationData _mapData = source._mapData + _compassMode = source._compassMode + _screenRgbColor = source._screenRgbColor + _isClockfaceAnalog = source._isClockfaceAnalog } } @@ -613,6 +700,9 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 13: try { try decoder.decodeSingularMessageField(value: &_storage._nodeHighlight) }() case 14: try { try decoder.decodeSingularBytesField(value: &_storage._calibrationData) }() case 15: try { try decoder.decodeSingularMessageField(value: &_storage._mapData) }() + case 16: try { try decoder.decodeSingularEnumField(value: &_storage._compassMode) }() + case 17: try { try decoder.decodeSingularUInt32Field(value: &_storage._screenRgbColor) }() + case 18: try { try decoder.decodeSingularBoolField(value: &_storage._isClockfaceAnalog) }() default: break } } @@ -670,6 +760,15 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement try { if let v = _storage._mapData { try visitor.visitSingularMessageField(value: v, fieldNumber: 15) } }() + if _storage._compassMode != .dynamic { + try visitor.visitSingularEnumField(value: _storage._compassMode, fieldNumber: 16) + } + if _storage._screenRgbColor != 0 { + try visitor.visitSingularUInt32Field(value: _storage._screenRgbColor, fieldNumber: 17) + } + if _storage._isClockfaceAnalog != false { + try visitor.visitSingularBoolField(value: _storage._isClockfaceAnalog, fieldNumber: 18) + } } try unknownFields.traverse(visitor: &visitor) } @@ -694,6 +793,9 @@ extension DeviceUIConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if _storage._nodeHighlight != rhs_storage._nodeHighlight {return false} if _storage._calibrationData != rhs_storage._calibrationData {return false} if _storage._mapData != rhs_storage._mapData {return false} + if _storage._compassMode != rhs_storage._compassMode {return false} + if _storage._screenRgbColor != rhs_storage._screenRgbColor {return false} + if _storage._isClockfaceAnalog != rhs_storage._isClockfaceAnalog {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 85981376..f46740fb 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -442,15 +442,15 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin case crowpanel // = 97 - ///* + /// /// Lilygo LINK32 board with sensors case link32 // = 98 - ///* + /// /// Seeed Tracker L1 case seeedWioTrackerL1 // = 99 - ///* + /// /// Seeed Tracker L1 EINK driver case seeedWioTrackerL1Eink // = 100 @@ -458,18 +458,30 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Reserved ID for future and past use case qwantzTinyArms // = 101 - ///* + /// /// Lilygo T-Deck Pro case tDeckPro // = 102 - ///* + /// /// Lilygo TLora Pager case tLoraPager // = 103 - ///* + /// /// GAT562 Mesh Trial Tracker case gat562MeshTrialTracker // = 104 + /// + /// RAKwireless WisMesh Tag + case wismeshTag // = 105 + + /// + /// RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ + case rak3312 // = 106 + + /// + /// Elecrow ThinkNode M5 https://www.elecrow.com/wiki/ThinkNode_M5_Meshtastic_LoRa_Signal_Transceiver_ESP32-S3.html + case thinknodeM5 // = 107 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -588,6 +600,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 102: self = .tDeckPro case 103: self = .tLoraPager case 104: self = .gat562MeshTrialTracker + case 105: self = .wismeshTag + case 106: self = .rak3312 + case 107: self = .thinknodeM5 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -700,6 +715,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .tDeckPro: return 102 case .tLoraPager: return 103 case .gat562MeshTrialTracker: return 104 + case .wismeshTag: return 105 + case .rak3312: return 106 + case .thinknodeM5: return 107 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -812,6 +830,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .tDeckPro, .tLoraPager, .gat562MeshTrialTracker, + .wismeshTag, + .rak3312, + .thinknodeM5, .privateHw, ] @@ -1059,7 +1080,7 @@ public enum ExcludedModules: SwiftProtobuf.Enum, Swift.CaseIterable { /// Paxcounter module case paxcounterConfig // = 4096 - /// + /// /// Bluetooth config (not technically a module, but used to indicate bluetooth capabilities) case bluetoothConfig // = 8192 @@ -3640,6 +3661,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 102: .same(proto: "T_DECK_PRO"), 103: .same(proto: "T_LORA_PAGER"), 104: .same(proto: "GAT562_MESH_TRIAL_TRACKER"), + 105: .same(proto: "WISMESH_TAG"), + 106: .same(proto: "RAK3312"), + 107: .same(proto: "THINKNODE_M5"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index 182e233c..03017f97 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -202,6 +202,12 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { /// ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface case reticulumTunnelApp // = 76 + /// + /// App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send + /// arbitrary telemetry over meshtastic that is not covered by telemetry.proto + /// ENCODING: CayenneLLP + case cayenneApp // = 77 + /// /// Private applications should use portnums >= 256. /// To simplify initial development and testing you can use "PRIVATE_APP" @@ -252,6 +258,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { case 73: self = .mapReportApp case 74: self = .powerstressApp case 76: self = .reticulumTunnelApp + case 77: self = .cayenneApp case 256: self = .privateApp case 257: self = .atakForwarder case 511: self = .max @@ -289,6 +296,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { case .mapReportApp: return 73 case .powerstressApp: return 74 case .reticulumTunnelApp: return 76 + case .cayenneApp: return 77 case .privateApp: return 256 case .atakForwarder: return 257 case .max: return 511 @@ -326,6 +334,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { .mapReportApp, .powerstressApp, .reticulumTunnelApp, + .cayenneApp, .privateApp, .atakForwarder, .max, @@ -365,6 +374,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 73: .same(proto: "MAP_REPORT_APP"), 74: .same(proto: "POWERSTRESS_APP"), 76: .same(proto: "RETICULUM_TUNNEL_APP"), + 77: .same(proto: "CAYENNE_APP"), 256: .same(proto: "PRIVATE_APP"), 257: .same(proto: "ATAK_FORWARDER"), 511: .same(proto: "MAX"), diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 58c21701..fba85796 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -21,7 +21,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) +/// But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) 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 @@ -30,7 +30,7 @@ public struct PowerMon: Sendable { public var unknownFields = SwiftProtobuf.UnknownStorage() /// 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. + /// If you are making new meshtastic features feel free to add new entries at the end of this definition. public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case none // = 0 @@ -57,8 +57,8 @@ public struct PowerMon: Sendable { case wifiOn // = 1024 /// - ///GPS is actively trying to find our location - ///See GPSPowerState for more details + /// GPS is actively trying to find our location + /// See GPSPowerState for more details case gpsActive // = 2048 case UNRECOGNIZED(Int) @@ -143,8 +143,8 @@ public struct PowerStressMessage: Sendable { /// /// What operation would we like the UUT to perform. - ///note: senders should probably set want_response in their request packets, so that they can know when the state - ///machine has started processing their request + /// note: senders should probably set want_response in their request packets, so that they can know when the state + /// machine has started processing their request public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index 2b89d4bd..c339fdbc 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -184,6 +184,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// PCT2075 Temperature Sensor case pct2075 // = 39 + + /// + /// ADS1X15 ADC + case ads1X15 // = 40 case UNRECOGNIZED(Int) public init() { @@ -232,6 +236,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case 37: self = .rak12035 case 38: self = .max17261 case 39: self = .pct2075 + case 40: self = .ads1X15 default: self = .UNRECOGNIZED(rawValue) } } @@ -278,6 +283,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case .rak12035: return 37 case .max17261: return 38 case .pct2075: return 39 + case .ads1X15: return 40 case .UNRECOGNIZED(let i): return i } } @@ -324,6 +330,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .rak12035, .max17261, .pct2075, + .ads1X15, ] } @@ -732,6 +739,116 @@ public struct PowerMetrics: Sendable { /// Clears the value of `ch3Current`. Subsequent reads from it will return its default value. public mutating func clearCh3Current() {self._ch3Current = nil} + /// + /// Voltage (Ch4) + public var ch4Voltage: Float { + get {return _ch4Voltage ?? 0} + set {_ch4Voltage = newValue} + } + /// Returns true if `ch4Voltage` has been explicitly set. + public var hasCh4Voltage: Bool {return self._ch4Voltage != nil} + /// Clears the value of `ch4Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh4Voltage() {self._ch4Voltage = nil} + + /// + /// Current (Ch4) + public var ch4Current: Float { + get {return _ch4Current ?? 0} + set {_ch4Current = newValue} + } + /// Returns true if `ch4Current` has been explicitly set. + public var hasCh4Current: Bool {return self._ch4Current != nil} + /// Clears the value of `ch4Current`. Subsequent reads from it will return its default value. + public mutating func clearCh4Current() {self._ch4Current = nil} + + /// + /// Voltage (Ch5) + public var ch5Voltage: Float { + get {return _ch5Voltage ?? 0} + set {_ch5Voltage = newValue} + } + /// Returns true if `ch5Voltage` has been explicitly set. + public var hasCh5Voltage: Bool {return self._ch5Voltage != nil} + /// Clears the value of `ch5Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh5Voltage() {self._ch5Voltage = nil} + + /// + /// Current (Ch5) + public var ch5Current: Float { + get {return _ch5Current ?? 0} + set {_ch5Current = newValue} + } + /// Returns true if `ch5Current` has been explicitly set. + public var hasCh5Current: Bool {return self._ch5Current != nil} + /// Clears the value of `ch5Current`. Subsequent reads from it will return its default value. + public mutating func clearCh5Current() {self._ch5Current = nil} + + /// + /// Voltage (Ch6) + public var ch6Voltage: Float { + get {return _ch6Voltage ?? 0} + set {_ch6Voltage = newValue} + } + /// Returns true if `ch6Voltage` has been explicitly set. + public var hasCh6Voltage: Bool {return self._ch6Voltage != nil} + /// Clears the value of `ch6Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh6Voltage() {self._ch6Voltage = nil} + + /// + /// Current (Ch6) + public var ch6Current: Float { + get {return _ch6Current ?? 0} + set {_ch6Current = newValue} + } + /// Returns true if `ch6Current` has been explicitly set. + public var hasCh6Current: Bool {return self._ch6Current != nil} + /// Clears the value of `ch6Current`. Subsequent reads from it will return its default value. + public mutating func clearCh6Current() {self._ch6Current = nil} + + /// + /// Voltage (Ch7) + public var ch7Voltage: Float { + get {return _ch7Voltage ?? 0} + set {_ch7Voltage = newValue} + } + /// Returns true if `ch7Voltage` has been explicitly set. + public var hasCh7Voltage: Bool {return self._ch7Voltage != nil} + /// Clears the value of `ch7Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh7Voltage() {self._ch7Voltage = nil} + + /// + /// Current (Ch7) + public var ch7Current: Float { + get {return _ch7Current ?? 0} + set {_ch7Current = newValue} + } + /// Returns true if `ch7Current` has been explicitly set. + public var hasCh7Current: Bool {return self._ch7Current != nil} + /// Clears the value of `ch7Current`. Subsequent reads from it will return its default value. + public mutating func clearCh7Current() {self._ch7Current = nil} + + /// + /// Voltage (Ch8) + public var ch8Voltage: Float { + get {return _ch8Voltage ?? 0} + set {_ch8Voltage = newValue} + } + /// Returns true if `ch8Voltage` has been explicitly set. + public var hasCh8Voltage: Bool {return self._ch8Voltage != nil} + /// Clears the value of `ch8Voltage`. Subsequent reads from it will return its default value. + public mutating func clearCh8Voltage() {self._ch8Voltage = nil} + + /// + /// Current (Ch8) + public var ch8Current: Float { + get {return _ch8Current ?? 0} + set {_ch8Current = newValue} + } + /// Returns true if `ch8Current` has been explicitly set. + public var hasCh8Current: Bool {return self._ch8Current != nil} + /// Clears the value of `ch8Current`. Subsequent reads from it will return its default value. + public mutating func clearCh8Current() {self._ch8Current = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -742,6 +859,16 @@ public struct PowerMetrics: Sendable { fileprivate var _ch2Current: Float? = nil fileprivate var _ch3Voltage: Float? = nil fileprivate var _ch3Current: Float? = nil + fileprivate var _ch4Voltage: Float? = nil + fileprivate var _ch4Current: Float? = nil + fileprivate var _ch5Voltage: Float? = nil + fileprivate var _ch5Current: Float? = nil + fileprivate var _ch6Voltage: Float? = nil + fileprivate var _ch6Current: Float? = nil + fileprivate var _ch7Voltage: Float? = nil + fileprivate var _ch7Current: Float? = nil + fileprivate var _ch8Voltage: Float? = nil + fileprivate var _ch8Current: Float? = nil } /// @@ -894,6 +1021,28 @@ public struct AirQualityMetrics: Sendable { /// Clears the value of `co2`. Subsequent reads from it will return its default value. public mutating func clearCo2() {self._co2 = nil} + /// + /// CO2 sensor temperature in degC + public var co2Temperature: Float { + get {return _co2Temperature ?? 0} + set {_co2Temperature = newValue} + } + /// Returns true if `co2Temperature` has been explicitly set. + public var hasCo2Temperature: Bool {return self._co2Temperature != nil} + /// Clears the value of `co2Temperature`. Subsequent reads from it will return its default value. + public mutating func clearCo2Temperature() {self._co2Temperature = nil} + + /// + /// CO2 sensor relative humidity in % + public var co2Humidity: Float { + get {return _co2Humidity ?? 0} + set {_co2Humidity = newValue} + } + /// Returns true if `co2Humidity` has been explicitly set. + public var hasCo2Humidity: Bool {return self._co2Humidity != nil} + /// Clears the value of `co2Humidity`. Subsequent reads from it will return its default value. + public mutating func clearCo2Humidity() {self._co2Humidity = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -911,6 +1060,8 @@ public struct AirQualityMetrics: Sendable { fileprivate var _particles50Um: UInt32? = nil fileprivate var _particles100Um: UInt32? = nil fileprivate var _co2: UInt32? = nil + fileprivate var _co2Temperature: Float? = nil + fileprivate var _co2Humidity: Float? = nil } /// @@ -1104,85 +1255,91 @@ public struct HostMetrics: Sendable { /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry: Sendable { +public struct Telemetry: @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. /// /// Seconds since 1970 - or 0 for unknown/unset - public var time: UInt32 = 0 + public var time: UInt32 { + get {return _storage._time} + set {_uniqueStorage()._time = newValue} + } - public var variant: Telemetry.OneOf_Variant? = nil + public var variant: OneOf_Variant? { + get {return _storage._variant} + set {_uniqueStorage()._variant = newValue} + } /// /// Key native device metrics such as battery level public var deviceMetrics: DeviceMetrics { get { - if case .deviceMetrics(let v)? = variant {return v} + if case .deviceMetrics(let v)? = _storage._variant {return v} return DeviceMetrics() } - set {variant = .deviceMetrics(newValue)} + set {_uniqueStorage()._variant = .deviceMetrics(newValue)} } /// /// Weather station or other environmental metrics public var environmentMetrics: EnvironmentMetrics { get { - if case .environmentMetrics(let v)? = variant {return v} + if case .environmentMetrics(let v)? = _storage._variant {return v} return EnvironmentMetrics() } - set {variant = .environmentMetrics(newValue)} + set {_uniqueStorage()._variant = .environmentMetrics(newValue)} } /// /// Air quality metrics public var airQualityMetrics: AirQualityMetrics { get { - if case .airQualityMetrics(let v)? = variant {return v} + if case .airQualityMetrics(let v)? = _storage._variant {return v} return AirQualityMetrics() } - set {variant = .airQualityMetrics(newValue)} + set {_uniqueStorage()._variant = .airQualityMetrics(newValue)} } /// /// Power Metrics public var powerMetrics: PowerMetrics { get { - if case .powerMetrics(let v)? = variant {return v} + if case .powerMetrics(let v)? = _storage._variant {return v} return PowerMetrics() } - set {variant = .powerMetrics(newValue)} + set {_uniqueStorage()._variant = .powerMetrics(newValue)} } /// /// Local device mesh statistics public var localStats: LocalStats { get { - if case .localStats(let v)? = variant {return v} + if case .localStats(let v)? = _storage._variant {return v} return LocalStats() } - set {variant = .localStats(newValue)} + set {_uniqueStorage()._variant = .localStats(newValue)} } /// /// Health telemetry metrics public var healthMetrics: HealthMetrics { get { - if case .healthMetrics(let v)? = variant {return v} + if case .healthMetrics(let v)? = _storage._variant {return v} return HealthMetrics() } - set {variant = .healthMetrics(newValue)} + set {_uniqueStorage()._variant = .healthMetrics(newValue)} } /// /// Linux host metrics public var hostMetrics: HostMetrics { get { - if case .hostMetrics(let v)? = variant {return v} + if case .hostMetrics(let v)? = _storage._variant {return v} return HostMetrics() } - set {variant = .hostMetrics(newValue)} + set {_uniqueStorage()._variant = .hostMetrics(newValue)} } public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -1213,6 +1370,8 @@ public struct Telemetry: Sendable { } public init() {} + + fileprivate var _storage = _StorageClass.defaultInstance } /// @@ -1281,6 +1440,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 37: .same(proto: "RAK12035"), 38: .same(proto: "MAX17261"), 39: .same(proto: "PCT2075"), + 40: .same(proto: "ADS1X15"), ] } @@ -1597,6 +1757,16 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 4: .standard(proto: "ch2_current"), 5: .standard(proto: "ch3_voltage"), 6: .standard(proto: "ch3_current"), + 7: .standard(proto: "ch4_voltage"), + 8: .standard(proto: "ch4_current"), + 9: .standard(proto: "ch5_voltage"), + 10: .standard(proto: "ch5_current"), + 11: .standard(proto: "ch6_voltage"), + 12: .standard(proto: "ch6_current"), + 13: .standard(proto: "ch7_voltage"), + 14: .standard(proto: "ch7_current"), + 15: .standard(proto: "ch8_voltage"), + 16: .standard(proto: "ch8_current"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1611,6 +1781,16 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 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 7: try { try decoder.decodeSingularFloatField(value: &self._ch4Voltage) }() + case 8: try { try decoder.decodeSingularFloatField(value: &self._ch4Current) }() + case 9: try { try decoder.decodeSingularFloatField(value: &self._ch5Voltage) }() + case 10: try { try decoder.decodeSingularFloatField(value: &self._ch5Current) }() + case 11: try { try decoder.decodeSingularFloatField(value: &self._ch6Voltage) }() + case 12: try { try decoder.decodeSingularFloatField(value: &self._ch6Current) }() + case 13: try { try decoder.decodeSingularFloatField(value: &self._ch7Voltage) }() + case 14: try { try decoder.decodeSingularFloatField(value: &self._ch7Current) }() + case 15: try { try decoder.decodeSingularFloatField(value: &self._ch8Voltage) }() + case 16: try { try decoder.decodeSingularFloatField(value: &self._ch8Current) }() default: break } } @@ -1639,6 +1819,36 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = self._ch3Current { try visitor.visitSingularFloatField(value: v, fieldNumber: 6) } }() + try { if let v = self._ch4Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 7) + } }() + try { if let v = self._ch4Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 8) + } }() + try { if let v = self._ch5Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 9) + } }() + try { if let v = self._ch5Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 10) + } }() + try { if let v = self._ch6Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 11) + } }() + try { if let v = self._ch6Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 12) + } }() + try { if let v = self._ch7Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 13) + } }() + try { if let v = self._ch7Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 14) + } }() + try { if let v = self._ch8Voltage { + try visitor.visitSingularFloatField(value: v, fieldNumber: 15) + } }() + try { if let v = self._ch8Current { + try visitor.visitSingularFloatField(value: v, fieldNumber: 16) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -1649,6 +1859,16 @@ extension PowerMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if lhs._ch2Current != rhs._ch2Current {return false} if lhs._ch3Voltage != rhs._ch3Voltage {return false} if lhs._ch3Current != rhs._ch3Current {return false} + if lhs._ch4Voltage != rhs._ch4Voltage {return false} + if lhs._ch4Current != rhs._ch4Current {return false} + if lhs._ch5Voltage != rhs._ch5Voltage {return false} + if lhs._ch5Current != rhs._ch5Current {return false} + if lhs._ch6Voltage != rhs._ch6Voltage {return false} + if lhs._ch6Current != rhs._ch6Current {return false} + if lhs._ch7Voltage != rhs._ch7Voltage {return false} + if lhs._ch7Current != rhs._ch7Current {return false} + if lhs._ch8Voltage != rhs._ch8Voltage {return false} + if lhs._ch8Current != rhs._ch8Current {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1670,6 +1890,8 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 11: .standard(proto: "particles_50um"), 12: .standard(proto: "particles_100um"), 13: .same(proto: "co2"), + 14: .standard(proto: "co2_temperature"), + 15: .standard(proto: "co2_humidity"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1691,6 +1913,8 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 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) }() + case 14: try { try decoder.decodeSingularFloatField(value: &self._co2Temperature) }() + case 15: try { try decoder.decodeSingularFloatField(value: &self._co2Humidity) }() default: break } } @@ -1740,6 +1964,12 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem try { if let v = self._co2 { try visitor.visitSingularUInt32Field(value: v, fieldNumber: 13) } }() + try { if let v = self._co2Temperature { + try visitor.visitSingularFloatField(value: v, fieldNumber: 14) + } }() + try { if let v = self._co2Humidity { + try visitor.visitSingularFloatField(value: v, fieldNumber: 15) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -1757,6 +1987,8 @@ extension AirQualityMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if lhs._particles50Um != rhs._particles50Um {return false} if lhs._particles100Um != rhs._particles100Um {return false} if lhs._co2 != rhs._co2 {return false} + if lhs._co2Temperature != rhs._co2Temperature {return false} + if lhs._co2Humidity != rhs._co2Humidity {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2011,154 +2243,196 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 8: .standard(proto: "host_metrics"), ] + fileprivate class _StorageClass { + var _time: UInt32 = 0 + var _variant: Telemetry.OneOf_Variant? + + #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) { + _time = source._time + _variant = source._variant + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularFixed32Field(value: &self.time) }() - case 2: try { - var v: DeviceMetrics? - var hadOneofValue = false - if let current = self.variant { - hadOneofValue = true - if case .deviceMetrics(let m) = current {v = m} + _ = _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.decodeSingularFixed32Field(value: &_storage._time) }() + case 2: try { + var v: DeviceMetrics? + var hadOneofValue = false + if let current = _storage._variant { + hadOneofValue = true + if case .deviceMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._variant = .deviceMetrics(v) + } + }() + case 3: try { + var v: EnvironmentMetrics? + var hadOneofValue = false + if let current = _storage._variant { + hadOneofValue = true + if case .environmentMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._variant = .environmentMetrics(v) + } + }() + case 4: try { + var v: AirQualityMetrics? + var hadOneofValue = false + if let current = _storage._variant { + hadOneofValue = true + if case .airQualityMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._variant = .airQualityMetrics(v) + } + }() + case 5: try { + var v: PowerMetrics? + var hadOneofValue = false + if let current = _storage._variant { + hadOneofValue = true + if case .powerMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._variant = .powerMetrics(v) + } + }() + case 6: try { + var v: LocalStats? + var hadOneofValue = false + if let current = _storage._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()} + _storage._variant = .localStats(v) + } + }() + case 7: try { + var v: HealthMetrics? + var hadOneofValue = false + if let current = _storage._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()} + _storage._variant = .healthMetrics(v) + } + }() + case 8: try { + var v: HostMetrics? + var hadOneofValue = false + if let current = _storage._variant { + hadOneofValue = true + if case .hostMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._variant = .hostMetrics(v) + } + }() + default: break } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.variant = .deviceMetrics(v) - } - }() - case 3: try { - var v: EnvironmentMetrics? - var hadOneofValue = false - if let current = self.variant { - hadOneofValue = true - if case .environmentMetrics(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.variant = .environmentMetrics(v) - } - }() - case 4: try { - var v: AirQualityMetrics? - var hadOneofValue = false - if let current = self.variant { - hadOneofValue = true - if case .airQualityMetrics(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.variant = .airQualityMetrics(v) - } - }() - case 5: try { - var v: PowerMetrics? - var hadOneofValue = false - if let current = self.variant { - hadOneofValue = true - if case .powerMetrics(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - 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) - } - }() - case 8: try { - var v: HostMetrics? - var hadOneofValue = false - if let current = self.variant { - hadOneofValue = true - if case .hostMetrics(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.variant = .hostMetrics(v) - } - }() - default: break } } } 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.time != 0 { - try visitor.visitSingularFixed32Field(value: self.time, fieldNumber: 1) - } - switch self.variant { - case .deviceMetrics?: try { - guard case .deviceMetrics(let v)? = self.variant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .environmentMetrics?: try { - guard case .environmentMetrics(let v)? = self.variant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case .airQualityMetrics?: try { - guard case .airQualityMetrics(let v)? = self.variant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .powerMetrics?: try { - 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 .hostMetrics?: try { - guard case .hostMetrics(let v)? = self.variant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - }() - case nil: break + 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._time != 0 { + try visitor.visitSingularFixed32Field(value: _storage._time, fieldNumber: 1) + } + switch _storage._variant { + case .deviceMetrics?: try { + guard case .deviceMetrics(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .environmentMetrics?: try { + guard case .environmentMetrics(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .airQualityMetrics?: try { + guard case .airQualityMetrics(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .powerMetrics?: try { + guard case .powerMetrics(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .localStats?: try { + guard case .localStats(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .healthMetrics?: try { + guard case .healthMetrics(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case .hostMetrics?: try { + guard case .hostMetrics(let v)? = _storage._variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() + case nil: break + } } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Telemetry, rhs: Telemetry) -> Bool { - if lhs.time != rhs.time {return false} - if lhs.variant != rhs.variant {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._time != rhs_storage._time {return false} + if _storage._variant != rhs_storage._variant {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true }