diff --git a/.github/workflows/macos-dSYM.yml b/.github/workflows/macos-dSYM.yml new file mode 100644 index 00000000..cb490792 --- /dev/null +++ b/.github/workflows/macos-dSYM.yml @@ -0,0 +1,21 @@ +name: Upload dSYM Files + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate/Download dSYM Files + uses: ./release.sh + + - name: Upload dSYMs to Datadog + uses: DataDog/upload-dsyms-github-action@v1 + 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 diff --git a/.github/workflows/sync_device_svgs.yml b/.github/workflows/sync_device_svgs.yml new file mode 100644 index 00000000..e1a1fd94 --- /dev/null +++ b/.github/workflows/sync_device_svgs.yml @@ -0,0 +1,161 @@ +name: Sync Device SVGs + +on: + schedule: + # Run nightly at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + # Allow manual triggering + +jobs: + sync-device-svgs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: | + npm install -g svgo + + - name: Download and process SVGs + run: | + #!/bin/bash + set -e + + # Create temporary directory + mkdir -p temp_svgs + cd temp_svgs + + # Clone web-flasher repo (shallow clone for speed) + git clone --depth 1 https://github.com/meshtastic/web-flasher.git + + # Navigate to SVG directory + cd web-flasher/public/img/devices + + # Create output directory + mkdir -p ../../../../processed_svgs + + # Process each SVG file + for svg_file in *.svg; do + if [ -f "$svg_file" ]; then + # Get filename without extension + filename=$(basename "$svg_file" .svg) + + # Optimize SVG + svgo "$svg_file" --output "../../../../processed_svgs/${filename}.svg" + + echo "Processed: $filename" + fi + done + + cd ../../../../ + ls -la processed_svgs/ + + - name: Update Xcode Assets + run: | + #!/bin/bash + set -e + + ASSETS_DIR="Meshtastic/Assets.xcassets" + + # Ensure assets directory exists + mkdir -p "$ASSETS_DIR" + + # Process each SVG + for svg_file in processed_svgs/*.svg; do + if [ -f "$svg_file" ]; then + # Get filename without extension + filename=$(basename "$svg_file" .svg) + + # Create imageset directory + imageset_dir="${ASSETS_DIR}/${filename}.imageset" + mkdir -p "$imageset_dir" + + # Copy SVG to imageset + cp "$svg_file" "${imageset_dir}/${filename}.svg" + + # Create Contents.json for the imageset + cat > "${imageset_dir}/Contents.json" << EOF + { + "images" : [ + { + "filename" : "${filename}.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } + } + EOF + + echo "Created imageset: ${filename}" + fi + done + + - name: Check for changes + id: check_changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.check_changes.outputs.has_changes == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add Meshtastic/Assets.xcassets/ + git commit -m "🤖 Sync device SVGs from web-flasher repo + + - Updated device images from meshtastic/web-flasher + - Automatically synced on $(date -u) + - Source: https://github.com/meshtastic/web-flasher/tree/main/public/img/devices" + git push + + - name: Create PR (alternative to direct push) + if: steps.check_changes.outputs.has_changes == 'true' && false # Set to true if you prefer PRs + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "🤖 Sync device SVGs from web-flasher repo" + title: "Sync device SVGs from web-flasher" + body: | + This PR automatically syncs device SVG images from the [meshtastic/web-flasher](https://github.com/meshtastic/web-flasher) repository. + + **Changes:** + - Updated device images from web-flasher repo + - Source: https://github.com/meshtastic/web-flasher/tree/main/public/img/devices + - Automatically generated on $(date -u) + + The SVGs have been optimized and converted to Xcode asset format. + branch: sync-device-svgs + delete-branch: true + + - name: Cleanup + if: always() + run: | + rm -rf temp_svgs processed_svgs + + - name: Summary + run: | + if [ "${{ steps.check_changes.outputs.has_changes }}" == "true" ]; then + echo "✅ Device SVGs updated successfully" + else + echo "ℹ️ No changes detected - SVGs are up to date" + fi \ No newline at end of file diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 05818ab7..96f34279 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -12,6 +12,12 @@ "value" : "%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "\t%@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -41,6 +47,12 @@ "value" : "%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -70,6 +82,12 @@ "value" : "%@%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " %@%%" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -87,6 +105,12 @@ "value" : ": %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -116,6 +140,12 @@ "value" : ": %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : ": %d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -145,6 +175,12 @@ "value" : "(Ri)definire il PIN_GPS_EN per la propria scheda." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボード用のPIN_GPS_ENを(再)定義してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -173,6 +209,12 @@ "value" : "%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -208,6 +250,12 @@ "value" : "%1$@ - %2$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -243,6 +291,12 @@ "value" : "%1$@ - %2$@ - %3$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ - %3$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -278,6 +332,12 @@ "value" : "%1$@ - %2$@ Verso %3$@ Indietro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ - %2$@ 送信 %3$@ 受信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -312,6 +372,12 @@ "value" : "%@ - Nessuna risposta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - 応答なし" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -346,6 +412,12 @@ "value" : "%@ - Non inviato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ - 送信されませんでした" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -380,6 +452,12 @@ "value" : "%1$@ (%2$@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ (%2$@)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -415,6 +493,12 @@ "value" : "%1$@ %2$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -450,6 +534,12 @@ "value" : "%1$@ %2$lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %2$lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -485,6 +575,12 @@ "value" : "%@ via" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 離れた場所" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -513,6 +609,12 @@ "value" : "%@ può essere lungo fino a %@ byte." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ は最大 %2$@ バイトまで設定できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -541,6 +643,12 @@ "value" : "%@ Canali?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ チャンネル?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -561,33 +669,8 @@ } } }, - "%@ config data was requested over the admin channel but no response has been returned from the remote node." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "i dati di configurazione %@ sono stati richiesti attraverso il canale di amministrazione, ma non è stata fornita alcuna risposta dal nodo remoto." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ конфигурациони подаци су затражени преко административног канала, али никакав одговор није враћен са удаљеног чвора." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "已通过管理频道请求 %@ 配置数据,但远程节点未返回任何响应。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "透過管理通道請求 %@ 組態資料,但遠端節點未回應。" - } - } - } + "%@ config data was requested via PKC admin but no response has been returned from the remote node." : { + }, "%@ dB" : { "localizations" : { @@ -597,6 +680,12 @@ "value" : "%@ dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -643,6 +732,12 @@ "value" : "%@ Si prega di provare a connettersi nuovamente e di controllare attentamente il PIN." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 再度接続を試行し、PINを慎重に確認してください。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -701,6 +796,12 @@ "value" : "%@ L'applicazione si riconnette automaticamente alla radio preferita se torna nel raggio d'azione." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 優先無線機が範囲内に戻った場合、アプリは自動的に再接続します。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -759,6 +860,12 @@ "value" : "%@ Questo errore di solito non può essere risolto senza dimenticare il dispositivo sotto Impostazioni > Bluetooth e riconnettersi alla radio." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ このエラーは通常、設定 > Bluetooth でデバイスの登録を解除し、無線機に再接続しない限り修正できません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -805,6 +912,12 @@ "value" : "%1$@, %2$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -840,6 +953,12 @@ "value" : "%1$@: %2$lld / %3$lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@: %2$lld / %3$lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -869,6 +988,12 @@ "value" : "%@%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -897,6 +1022,12 @@ "value" : "%@°F" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@°F" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -925,6 +1056,12 @@ "value" : "%@mA" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@mA" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -947,6 +1084,12 @@ "value" : "%@V" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@V" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -969,6 +1112,12 @@ "value" : "%d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1033,6 +1182,12 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%dホップ" + } + }, "sr" : { "variations" : { "plural" : { @@ -1091,6 +1246,12 @@ "value" : "%d%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1119,6 +1280,12 @@ "value" : "%lf" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lf" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1147,6 +1314,12 @@ "value" : "%lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1181,6 +1354,12 @@ "value" : "%lld o meno hops di distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldホップ以下" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1203,6 +1382,12 @@ "value" : "%lld Letture Totale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "計 %lld 件の読み取り値" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1225,6 +1410,12 @@ "value" : "%lld Totale eventi di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "計 %lld 件の検出イベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1247,6 +1438,12 @@ "value" : "%lld%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1281,6 +1478,12 @@ "value" : "%llddb Potenza di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddb送信電力" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1315,6 +1518,12 @@ "value" : "%llddBm Potenza di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%llddBm送信電力" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1329,6 +1538,9 @@ } } }, + "• %@" : { + "shouldTranslate" : false + }, "< 1%" : { "localizations" : { "it" : { @@ -1337,6 +1549,12 @@ "value" : "< 1%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "< 1%" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1362,7 +1580,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "🦕 Versione di fine vita 🦖 ☄️" + "value" : "🦕 Versione a fine vita 🦖 ☄️" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "🦕 サポート終了バージョン 🦖 ☄️" } }, "sr" : { @@ -1379,6 +1603,26 @@ } } }, + "0" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "0" + } + } + } + }, + "1" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1" + } + } + } + }, "1 byte" : { "localizations" : { "it" : { @@ -1387,6 +1631,12 @@ "value" : "1 byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1バイト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1415,6 +1665,12 @@ "value" : "a 1 salto di distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1ホップ先" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1443,6 +1699,12 @@ "value" : "2.4 Ghz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2.4GHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1465,6 +1727,12 @@ "value" : "7" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "7" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1493,6 +1761,12 @@ "value" : "8" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "8" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1513,6 +1787,16 @@ } } }, + "12 Hour Clock" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "12時間表示" + } + } + } + }, "25" : { "localizations" : { "it" : { @@ -1521,6 +1805,12 @@ "value" : "25" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "25" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1549,6 +1839,12 @@ "value" : "50" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "50" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1577,6 +1873,12 @@ "value" : "75" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "75" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1605,6 +1907,12 @@ "value" : "100" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1633,6 +1941,12 @@ "value" : "128 bit" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "128 bit" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1653,6 +1967,16 @@ } } }, + "180" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "180" + } + } + } + }, "256 bit" : { "localizations" : { "it" : { @@ -1661,6 +1985,12 @@ "value" : "256 bit" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "256 bit" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1681,6 +2011,19 @@ } } }, + "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." : { + + }, + "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緑色の鍵は、チャンネルが128ビットまたは256ビットのAESキーで安全に暗号化されていることを意味します。" + } + } + } + }, "A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio." : { "localizations" : { "de" : { @@ -1707,6 +2050,12 @@ "value" : "Un codice QR Meshtastic contiene la configurazione LoRa e i valori dei canali necessari alle radio per comunicare. È possibile condividere una configurazione completa dei canali utilizzando l'opzione Sostituisci canali; se si sceglie Aggiungi canali, i canali condivisi verranno aggiunti ai canali della radio ricevente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeshtasticのQRコードには、無線機が通信するために必要なLoRa設定とチャンネル値が含まれています。「チャンネルを置換」オプションを使用して完全なチャンネル設定を共有できます。「チャンネルを追加」を選択した場合、共有チャンネルは受信側無線機のチャンネルに追加されます。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -1738,6 +2087,12 @@ } } } + }, + "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." : { + + }, + "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." : { + }, "A Trace Route was sent, no response has been received." : { "localizations" : { @@ -1747,6 +2102,12 @@ "value" : "È stata inviata una rotta di tracciamento, ma non è stata ricevuta alcuna risposta." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trace Route が送信されましたが、応答が受信されませんでした。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1760,13 +2121,22 @@ } } } + }, + "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." : { + }, "About" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "Circa" + "value" : "Informazioni" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "概要" } }, "sr" : { @@ -1791,6 +2161,12 @@ "value" : "Informazioni su Meshtastic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticについて" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1819,6 +2195,12 @@ "value" : "Precisione %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "精度 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1841,6 +2223,12 @@ "value" : "SNR Ack: %@ dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答SNR: %@ dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1863,6 +2251,12 @@ "value" : "Tempo di risposta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答時間: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1900,7 +2294,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riconosciuto" + "value" : "Confermato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認済み" } }, "pl" : { @@ -1940,7 +2340,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riconosciuto da un altro nodo" + "value" : "Confermato da un altro nodo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "他のノードで確認済み" } }, "sr" : { @@ -1971,6 +2377,12 @@ "value" : "Azioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -1999,6 +2411,12 @@ "value" : "Attivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクティブ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2027,6 +2445,12 @@ "value" : "Attività" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクティビティ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2067,6 +2491,12 @@ "value" : "Override ADC" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ADC上書き" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -2101,6 +2531,12 @@ "value" : "Aggiungi canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルを追加" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2123,6 +2559,12 @@ "value" : "Aggiungi canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルを追加" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2137,6 +2579,44 @@ } } }, + "Add Contact" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先を追加" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "增加聯絡人" + } + } + } + }, + "Add Meshtastic Node %@ as a contact" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi nodo Meshtastic %@ ai contatti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticノード%@を連絡先に追加" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "將 Meshtastic 節點 %@ 新增為聯絡人" + } + } + } + }, "Add to favorites" : { "localizations" : { "de" : { @@ -2151,6 +2631,12 @@ "value" : "Aggiungi ai preferiti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りに追加" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2173,6 +2659,12 @@ "value" : "Aiuto supplementare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追加のヘルプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2201,6 +2693,12 @@ "value" : "Indirizzo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "住所" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2221,36 +2719,12 @@ } } }, - "Admin & Direct Message Keys" : { + "Admin Keys" : { "localizations" : { - "de" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Schlüssel für Administrator und Direktnachrichten" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tasti amministratore e messaggi diretti" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Админ и кључеви директних порука" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "管理员 & 私信密钥" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "管理與直接訊息加密金鑰" + "value" : "管理者キー" } } } @@ -2263,6 +2737,12 @@ "value" : "Amministrazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2284,7 +2764,20 @@ } }, "Administration Enabled" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理機能が有効" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理功能已啟用" + } + } + } }, "Advanced" : { "localizations" : { @@ -2294,6 +2787,12 @@ "value" : "Avanzato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上級" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2322,6 +2821,12 @@ "value" : "Dispositivo avanzato GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度なデバイスGPS" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2350,6 +2855,12 @@ "value" : "Opzioni GPIO avanzate" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度なGPIOオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2378,6 +2889,12 @@ "value" : "Flags di posizione avanzati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度な位置フラグ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2412,6 +2929,12 @@ "value" : "Dopo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "後" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -2432,6 +2955,40 @@ } } }, + "After %lld Days" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "After %lld Day" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "After %lld Days" + } + } + } + } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "あと %lld 日" + } + } + } + } + } + } + }, "After config values save the node will reboot." : { "localizations" : { "de" : { @@ -2458,6 +3015,12 @@ "value" : "Dopo il salvataggio dei valori di configurazione, il nodo si riavvia." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定値の保存後、ノードは再起動します。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -2504,6 +3067,12 @@ "value" : "Pomeriggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "午後" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2541,7 +3110,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ora d'aria" + "value" : "Tempo di trasmissione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エアタイム" } }, "pl" : { @@ -2581,7 +3156,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Allarme" + "value" : "Avviso" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラート" } }, "sr" : { @@ -2603,7 +3184,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avvisare il buzzer GPIO quando si riceve un campanello" + "value" : "Attiva il cicalino GPIO alla ricezione di una campana" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル受信時にGPIOブザーでアラート" } }, "sr" : { @@ -2631,7 +3218,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avvisare il cicalino GPIO quando si riceve un messaggio" + "value" : "Attiva il cicalino GPIO alla ricezione di un messaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ受信時にGPIOブザーでアラート" } }, "sr" : { @@ -2653,7 +3246,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso GPIO del motore a vibrazione quando si riceve una campana" + "value" : "Attiva la vibrazione GPIO alla ricezione di una campana" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル受信時にGPIO振動モーターでアラート" } }, "sr" : { @@ -2681,7 +3280,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso GPIO del motore vibrante alla ricezione di un messaggio" + "value" : "Attiva la vibrazione GPIO alla ricezione di un messaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ受信時にGPIO振動モーターでアラート" } }, "sr" : { @@ -2703,7 +3308,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso di ricezione di un campanello" + "value" : "Avvisa alla ricezione di una campana" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル受信時にアラート" } }, "sr" : { @@ -2731,7 +3342,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso di ricezione di un messaggio" + "value" : "Avvisa alla ricezione di un messaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ受信時にアラート" } }, "sr" : { @@ -2762,6 +3379,12 @@ "value" : "Tutti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全て" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2782,68 +3405,18 @@ } } }, - "All device and app data will be deleted." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tutti i dati del dispositivo e delle app verranno eliminati." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сви подаци о уређају и апликацији ће бити избрисани." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "所有设备以及 App 数据都会被删除。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "全部的設備及App資料將會被刪除。" - } - } - } - }, - "Allow incoming device control over the insecure legacy admin channel." : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erlaubt die eingehende Gerätesteuerung über den unsicheren Legacy-Admin-Kanal." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Consentire il controllo del dispositivo in entrata attraverso il canale di amministrazione legacy non sicuro." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дозволите контролу долазног уређаја над небезбедним старим администраторским каналом." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "允許經由不安全的傳統管理通道接收裝置控制指令。" - } - } - } - }, "Allow Position Requests" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "Consentire le richieste di posizione" + "value" : "Consenti le richieste di posizione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置要求を許可" } }, "sr" : { @@ -2868,6 +3441,12 @@ "value" : "Alt" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2896,6 +3475,12 @@ "value" : "Altitudine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2924,6 +3509,12 @@ "value" : "Altitudine %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2946,6 +3537,12 @@ "value" : "Altitudine Separazione geoidale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度ジオイド分離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -2968,6 +3565,12 @@ "value" : "L'altitudine è il livello medio del mare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高度は平均海面レベル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3008,6 +3611,12 @@ "value" : "Sempre acceso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常にオン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3054,6 +3663,12 @@ "value" : "Punta sempre verso nord" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常に北を指す" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3094,6 +3709,12 @@ "value" : "Illuminazione ambientale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境照明" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -3146,6 +3767,12 @@ "value" : "Configurazione dell'illuminazione ambientale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境照明設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -3192,6 +3819,12 @@ "value" : "Configurazione del modulo di illuminazione ambientale ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アンビエントライトモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3238,6 +3871,12 @@ "value" : "Una rete mesh open source, off-grid, decentralizzata, che funziona con radio a basso costo e a bassa potenza." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手頃な価格の低電力無線機で動作する、オープンソース、オフグリッド、分散型メッシュネットワーク。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3266,6 +3905,12 @@ "value" : "I messaggi persi saranno consegnati nuovamente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "見逃したメッセージは再配信されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3312,6 +3957,12 @@ "value" : "Dispositivo di messaggistica collegato all'app o indipendente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ接続または独立型メッセージングデバイス。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3352,6 +4003,12 @@ "value" : "Dati dell'applicazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App データ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3380,6 +4037,12 @@ "value" : "File dell'applicazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリファイル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3411,6 +4074,12 @@ "value" : "Impostazioni dell'app" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3439,6 +4108,12 @@ "value" : "Applicazioni Apple" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appleアプリ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3473,6 +4148,12 @@ "value" : "Posizione approssimativa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "正確な位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3495,6 +4176,12 @@ "value" : "Sei sicuro di voler cancellare questo messaggio?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージを削除してもよろしいですか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3529,6 +4216,12 @@ "value" : "Siete sicuri di voler resettare il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを工場出荷時設定にリセットしてもよろしいですか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3575,6 +4268,12 @@ "value" : "Sei sicuro?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "本当によろしいですか?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3615,6 +4314,12 @@ "value" : "Australia / Nuova Zelanda" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オーストラリア / ニュージーランド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3637,6 +4342,12 @@ "value" : "Passa automaticamente alla pagina successiva sullo schermo come un carosello, in base all'intervallo specificato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "指定した間隔に基づいて、カルーセルのように画面の次のページに自動的に切り替わります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3665,6 +4376,12 @@ "value" : "Preimpostazioni modem disponibili, l'impostazione predefinita è Lungo veloce." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用可能なモデムプリセット、デフォルトは Long Fast です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3711,6 +4428,12 @@ "value" : "Radio disponibili" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用可能な無線機" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3769,6 +4492,12 @@ "value" : "Indietro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "戻る" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -3801,6 +4530,26 @@ } } }, + "Backup" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バックアップ" + } + } + } + }, + "Backup your private key to your iCloud keychain." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートキーをiCloudキーチェーンにバックアップします。" + } + } + } + }, "Bad" : { "localizations" : { "it" : { @@ -3809,6 +4558,12 @@ "value" : "Pessimo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "悪い" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3840,7 +4595,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Richiesta negativa" + "value" : "Richiesta non valida" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不正なリクエスト" } }, "pl" : { @@ -3889,6 +4650,12 @@ "value" : "Larghezza di banda" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "帯域幅" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3917,6 +4684,12 @@ "value" : "Bar" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3945,6 +4718,12 @@ "value" : "Serie Bar" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バー系列" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -3973,6 +4752,12 @@ "value" : "Pressione barometrica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "気圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4013,6 +4798,12 @@ "value" : "Batteria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッテリー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4046,6 +4837,7 @@ } }, "Battery Level" : { + "comment" : "VoiceOver label for battery gauge", "localizations" : { "de" : { "stringUnit" : { @@ -4071,6 +4863,12 @@ "value" : "Livello della batteria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッテリーレベル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4104,6 +4902,7 @@ } }, "Battery Level %" : { + "comment" : "VoiceOver value for battery level", "localizations" : { "it" : { "stringUnit" : { @@ -4111,6 +4910,12 @@ "value" : "Livello della batteria %" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バッテリーレベル %" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4139,6 +4944,12 @@ "value" : "Baud" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボーレート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4159,40 +4970,6 @@ } } }, - "biking" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "biken" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "tour in bicicletta" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "тура бициклом" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "自行车旅行" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "自行車" - } - } - } - }, "Biking" : { "localizations" : { "de" : { @@ -4207,6 +4984,12 @@ "value" : "In bicicletta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サイクリング" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4235,6 +5018,12 @@ "value" : "BLE" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -4281,6 +5070,12 @@ "value" : "Nome BLE" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE 名前" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4339,6 +5134,12 @@ "value" : "Il pin BLE deve essere composto da 6 cifre." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE PINは6桁である必要があります。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4371,30 +5172,12 @@ } } }, - "BLE RSSI: %lld" : { + "BLE RSSI %lld" : { "localizations" : { - "it" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "RSSI BLE: %lld" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "BLE RSSI: %lld" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "BLE RSSI: %lld" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "藍牙訊號強度(RSSI):%lld" + "value" : "BLE RSSI %lld" } } } @@ -4407,6 +5190,12 @@ "value" : "BLE: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "BLE: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4453,6 +5242,12 @@ "value" : "Bluetooth" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4511,6 +5306,12 @@ "value" : "Configurazione Bluetooth" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4569,6 +5370,12 @@ "value" : "Configurazione Bluetooth ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4627,6 +5434,12 @@ "value" : "Il Bluetooth è spento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetoothがオフです" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4667,6 +5480,12 @@ "value" : "Intervallo di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブロードキャスト間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4710,7 +5529,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Trasmette i pacchetti di posizione GPS come priorità." + "value" : "Dà priorità alla trasmissione di pacchetti di posizione GPS." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS位置パケットを優先的にブロードキャストします。" } }, "pl" : { @@ -4768,7 +5593,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per assistere il recupero del dispositivo." + "value" : "Trasmette regolarmente la posizione come messaggio al canale predefinito per aiutare il recupero del dispositivo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスの回復を支援するため、定期的に位置情報をデフォルトチャンネルにメッセージとしてブロードキャストします。" } }, "pl" : { @@ -4829,6 +5660,12 @@ "value" : "Trasmette i pacchetti di telemetria come priorità." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリパケットを優先的にブロードキャストします。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4869,6 +5706,12 @@ "value" : "Pulsante GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボタンGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -4894,7 +5737,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Acquistare radio complete" + "value" : "Acquista dispositivi completi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成品無線機を購入" } }, "sr" : { @@ -4922,7 +5771,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Buzzer GPIO" + "value" : "Cicalino GPIO" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブザーGPIO" } }, "sr" : { @@ -4947,6 +5802,12 @@ }, "By enabling this feature, you acknowledge and expressly consent to the transmission of your device’s real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この機能を有効にすることで、お客様のデバイスのリアルタイム地理位置が暗号化されずにMQTTプロトコル経由で送信されることを承知し、明示的に同意することを認めます。この位置データは、ライブマップ報告、デバイス追跡、関連テレメトリー機能などの目的で使用される場合があります。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -4981,6 +5842,12 @@ "value" : "Byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バイト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5013,6 +5880,17 @@ } } }, + "Bytes Used" : { + "comment" : "VoiceOver value for bytes used", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用バイト" + } + } + } + }, "Call Sign" : { "localizations" : { "it" : { @@ -5021,6 +5899,12 @@ "value" : "Segnale di chiamata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コールサイン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5049,6 +5933,12 @@ "value" : "Il nominativo non deve essere vuoto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コールサインは空にできません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5095,6 +5985,12 @@ "value" : "Annullamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キャンセル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5147,6 +6043,12 @@ "value" : "Configurazione del modulo Canned Message ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5205,6 +6107,12 @@ "value" : "Messaggi in scatola" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5263,6 +6171,12 @@ "value" : "Configurazione dei messaggi in scatola" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージ設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5315,6 +6229,12 @@ "value" : "Messaggi in scatola Messaggi ricevuti per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "定型メッセージ受信対象: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5355,6 +6275,12 @@ "value" : "Intervallo del carosello" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カルーセル間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5383,6 +6309,12 @@ "value" : "Categorie" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カテゴリ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5417,6 +6349,12 @@ "value" : "Categoria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カテゴリ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5439,6 +6377,12 @@ "value" : "Corrente Ch1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch1 現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5461,6 +6405,12 @@ "value" : "Tensione Ch1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch1 電圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5483,6 +6433,12 @@ "value" : "Corrente Ch2" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch2 現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5505,6 +6461,12 @@ "value" : "Tensione Ch2" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch2 電圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5527,6 +6489,12 @@ "value" : "Corrente Ch3" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch3 現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5549,6 +6517,12 @@ "value" : "Tensione Ch3" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ch3 電圧" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5589,6 +6563,12 @@ "value" : "Canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -5629,6 +6609,12 @@ "value" : "Canale 0 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル0を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5657,6 +6643,12 @@ "value" : "Canale 1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル1" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5679,6 +6671,12 @@ "value" : "Canale 1 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル1を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5707,6 +6705,12 @@ "value" : "Canale 2" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル2" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5729,6 +6733,12 @@ "value" : "Canale 2 incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル2を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5757,6 +6767,12 @@ "value" : "Canale 3" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 3" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5779,6 +6795,12 @@ "value" : "Canale 3 incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 3を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5807,6 +6829,12 @@ "value" : "Canale 4 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 4を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5835,6 +6863,12 @@ "value" : "Canale 5 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 5を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5863,6 +6897,12 @@ "value" : "Canale 6 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 6を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5891,6 +6931,12 @@ "value" : "Canale 7 Incluso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル 7を含む" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5919,6 +6965,12 @@ "value" : "dettagli del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル詳細" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5947,6 +6999,12 @@ "value" : "Nome del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル名" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -5975,6 +7033,12 @@ "value" : "Il numero del canale deve essere compreso tra 0 e 7." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル番号は0から7の間である必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6003,6 +7067,12 @@ "value" : "Ruolo del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6031,6 +7101,12 @@ "value" : "URL del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル URL" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6071,6 +7147,12 @@ "value" : "Utilizzo del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル使用率" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6111,6 +7193,12 @@ "value" : "Utilizzo del canale %@%%" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル使用率 %@%%" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6145,6 +7233,12 @@ "value" : "Canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6185,6 +7279,12 @@ "value" : "I canali aggiunti dal codice QR non venivano salvati. Quando si aggiungono canali, i nomi devono essere unici." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコードから追加されたチャンネルが保存されませんでした。チャンネルを追加する際は、名前が一意である必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6205,6 +7305,16 @@ } } }, + "Channels Help" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルヘルプ" + } + } + } + }, "Chart" : { "localizations" : { "it" : { @@ -6213,6 +7323,12 @@ "value" : "Grafico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6241,6 +7357,12 @@ "value" : "CHG" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "充電中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6263,6 +7385,12 @@ "value" : "Cina" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中国" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6282,7 +7410,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Libero" + "value" : "Svuota" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリア" } }, "sr" : { @@ -6331,6 +7465,12 @@ "value" : "Cancella i dati dell'app" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリア App Data" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6371,6 +7511,12 @@ "value" : "Cancella registro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログクリア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6385,6 +7531,16 @@ } } }, + "Clear Stale Nodes" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "古いノードをクリア" + } + } + } + }, "Client" : { "localizations" : { "it" : { @@ -6393,6 +7549,12 @@ "value" : "Cliente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6427,6 +7589,12 @@ "value" : "Cliente Nascosto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント Hidden" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6455,6 +7623,12 @@ "value" : "Storia del cliente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント履歴" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6483,6 +7657,12 @@ "value" : "Richiesta di cronologia clienti inviata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント履歴リクエストを送信しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6511,6 +7691,12 @@ "value" : "Cliente Muto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアント無音" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6539,6 +7725,12 @@ "value" : "Opzioni del cliente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クライアントオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6567,6 +7759,12 @@ "value" : "Evento rotariano in senso orario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時計回りロータリーイベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6613,6 +7811,12 @@ "value" : "Chiudere" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6653,6 +7857,12 @@ "value" : "Tasso di codifica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "符号化率" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6687,6 +7897,12 @@ "value" : "Colore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "色" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6715,6 +7931,12 @@ "value" : "Comunicare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通信中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6737,6 +7959,12 @@ "value" : "Supporto alla community" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティサポート" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6753,6 +7981,12 @@ "value" : "Configurazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6787,6 +8021,12 @@ "value" : "Configurazione per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ の設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6809,6 +8049,12 @@ "value" : "Preset di configurazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定プリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6843,6 +8089,12 @@ "value" : "Configurare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定する" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6868,6 +8120,12 @@ "value" : "Conferma" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6890,6 +8148,12 @@ "value" : "Collegarsi a un nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードに接続" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -6906,6 +8170,12 @@ }, "Connect to MQTT via Proxy" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロキシ経由でMQTTに接続" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6922,6 +8192,12 @@ "value" : "Collegare alla nuova radio?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しい無線機に接続しますか?" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -6956,6 +8232,12 @@ "value" : "Bluetooth collegato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7002,6 +8284,12 @@ "value" : "Nodo collegato %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続済みノード %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7021,7 +8309,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Radio connessa" + "value" : "Dispositivo connesso" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続済み無線機" } }, "zh-Hant-TW" : { @@ -7058,6 +8352,12 @@ "value" : "Collegamento. ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続中..." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7095,7 +8395,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La connessione a una nuova radio cancellerà tutti i dati delle app sul telefono." + "value" : "La connessione a un nuovo dispositivo cancellerà tutti i dati dell'applicazione sul telefono." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しい無線機に接続すると、電話上の全てのアプリデータがクリアされます。" } }, "zh-Hant-TW" : { @@ -7120,6 +8426,12 @@ "value" : "Tentativo di connessione %lld di 10" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続試行 %lld / 10" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7166,6 +8478,12 @@ "value" : "Connessione fallita dopo %d tentativi di connessione a %@. Potrebbe essere necessario disaccoppiare il tuo dispositivo in Impostazioni > Bluetooth." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d への接続が %@ 回の試行後に失敗しました。設定 > Bluetooth でデバイスを削除する必要があるかもしれません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7200,6 +8518,12 @@ }, "Consent to Share Unencrypted Node Data via MQTT" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT経由での暗号化されていないノードデータの共有に同意" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -7208,6 +8532,22 @@ } } }, + "Contact URL" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先URL" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "聯絡人網址" + } + } + } + }, "Contacts (%@)" : { "localizations" : { "de" : { @@ -7234,6 +8574,12 @@ "value" : "Contatti (%@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先s (%@)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7277,6 +8623,12 @@ "value" : "Tipo di controllo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Control タイプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7302,7 +8654,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre i LED del caricatore e del GPS non sono controllabili." + "value" : "Controlla il LED lampeggiante del dispositivo. Per la maggior parte dei dispositivi controlla uno dei 4 LED, mentre quelli di alimentazione e del GPS non sono controllabili." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス上の点滅LEDを制御します。ほとんどのデバイスでは最大4つのLEDのうち1つを制御し、充電器とGPS LEDは制御できません。" } }, "sr" : { @@ -7336,7 +8694,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Scafo convesso" + "value" : "Inviluppo convesso" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "凸包" } }, "sr" : { @@ -7364,7 +8728,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Coordinare" + "value" : "Coordinate" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "座標" } }, "sr" : { @@ -7395,6 +8765,12 @@ "value" : "Coordinate %@, %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "座標 %@, %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7429,6 +8805,12 @@ "value" : "Coordinate:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "座標:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7469,6 +8851,12 @@ "value" : "Copia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コピー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7515,6 +8903,12 @@ "value" : "Impossibile trovare il nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードが見つかりません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7543,6 +8937,12 @@ "value" : "Evento rotativo antiorario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "反時計回りの回転イベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7571,6 +8971,12 @@ "value" : "Creare un waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントを作成" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7599,6 +9005,12 @@ "value" : "Creato: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "作成日時: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7624,6 +9036,12 @@ "value" : "Attuale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7652,6 +9070,12 @@ "value" : "Versione attuale del firmware: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のファームウェアバージョン: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7686,6 +9110,12 @@ "value" : "Versione attuale del firmware: %@, Ultima versione del firmware: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のファームウェアバージョン: %@、最新のファームウェアバージョン: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7717,7 +9147,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Corrente: %lld" + "value" : "Attuale: %lld" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在: %lld" } }, "sr" : { @@ -7739,7 +9175,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Attualmente mostra i moduli che potrebbero non essere supportati da questo nodo." + "value" : "Mostra i moduli che potrebbero non essere supportati al momento da questo nodo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在、このノードでサポートされていない可能性のあるモジュールを表示しています。" } }, "zh-Hant-TW" : { @@ -7758,6 +9200,12 @@ "value" : "Attualmente il modo consigliato per aggiornare i dispositivi ESP32 è quello di utilizzare il flasher web su un computer desktop da un browser basato su chrome. Non funziona su dispositivi mobili o tramite BLE." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在、ESP32デバイスの更新の推奨方法は、デスクトップコンピューターのChromeベースのブラウザーでWebフラッシャーを使用することです。モバイルデバイスやBLE経由では動作しません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7792,6 +9240,12 @@ "value" : "Data" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日付" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7814,6 +9268,12 @@ "value" : "Debug" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバッグ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7848,6 +9308,12 @@ "value" : "Registri di debug" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバッグログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7870,6 +9336,12 @@ "value" : "Registri di debug%@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバッグログ%@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -7910,6 +9382,12 @@ "value" : "Formato dei gradi decimali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "十進度形式" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7968,6 +9446,12 @@ "value" : "Predefinito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デフォルト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8008,6 +9492,12 @@ "value" : "Layout dello schermo 128x64 predefinito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デフォルト128x64スクリーンレイアウト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8054,6 +9544,12 @@ "value" : "Gradi Minuti Secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "度分秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8112,6 +9608,12 @@ "value" : "Cancellare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8144,6 +9646,38 @@ } } }, + "Delete all config, keys and BLE bonds? " : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare tutte le configurazioni, le chiavi e le associazioni bluetooth?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての設定、キー、BLEボンドを削除しますか?" + } + } + } + }, + "Delete all config? " : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancellare tutte le configurazioni?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての設定を削除しますか?" + } + } + } + }, "Delete all device metrics?" : { "localizations" : { "fr" : { @@ -8164,6 +9698,12 @@ "value" : "Cancellare tutte le metriche del dispositivo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのデバイスメトリクスを削除しますか?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8204,6 +9744,12 @@ "value" : "Cancellare tutte le metriche dell'ambiente?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての環境メトリクスを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8232,6 +9778,12 @@ "value" : "Cancellare tutti i dati dei passeggeri?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全てのPAXデータを削除しますか?" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -8260,6 +9812,12 @@ "value" : "Cancellare tutte le posizioni?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全ての位置データを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8282,6 +9840,12 @@ "value" : "Cancellare il messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8304,6 +9868,12 @@ "value" : "Cancellare i messaggi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8332,6 +9902,12 @@ "value" : "Cancellare il nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8360,6 +9936,12 @@ "value" : "Cancellare il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8382,6 +9964,12 @@ "value" : "Cancellare le metriche di potenza?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスを削除しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8404,6 +9992,12 @@ "value" : "Descrizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "説明" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8426,6 +10020,12 @@ "value" : "La descrizione deve essere inferiore a 100 byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "説明は100バイト未満である必要があります" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8454,6 +10054,12 @@ "value" : "Rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8476,6 +10082,12 @@ "value" : "Evento di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出イベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8517,6 +10129,12 @@ "value" : "Sensore di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8569,6 +10187,12 @@ "value" : "Configurazione del sensore di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサー設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -8603,6 +10227,12 @@ "value" : "Registro del sensore di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8625,6 +10255,12 @@ "value" : "I messaggi del sensore di rilevamento vengono ricevuti come messaggi di testo. Se si attivano le notifiche, si riceverà una notifica per ogni messaggio di rilevamento ricevuto e un badge per il messaggio non letto corrispondente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーメッセージはテキストメッセージとして受信されます。通知を有効にすると、受信した各検出メッセージの通知と対応する未読メッセージバッジが表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8665,6 +10301,12 @@ "value" : "Configurazione del modulo sensore di rilevamento ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8705,6 +10347,12 @@ "value" : "Sviluppatori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開発者" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8751,6 +10399,12 @@ "value" : "Dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8809,6 +10463,12 @@ "value" : "Configurazione del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8867,6 +10527,12 @@ "value" : "Configurazione dispositivo ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8913,6 +10579,12 @@ "value" : "Configurazione del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -8953,6 +10625,12 @@ "value" : "Dispositivo GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス GPS" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -8981,6 +10659,12 @@ "value" : "Il dispositivo è gestito da un amministratore di rete, ma l'utente non può accedere alle impostazioni del dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスはメッシュ管理者によって管理されており、ユーザーはデバイス設定にアクセスできません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9027,6 +10711,12 @@ "value" : "Metadati del dispositivo ricevuti da: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメタデータを受信: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9067,6 +10757,12 @@ "value" : "Metriche del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9095,6 +10791,12 @@ "value" : "Registro delle metriche del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクスログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9129,6 +10831,12 @@ "value" : "Modello dispositivo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスモデル: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9157,6 +10865,12 @@ "value" : "Ruolo del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9185,6 +10899,12 @@ "value" : "Schermata del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9231,6 +10951,12 @@ "value" : "Dispositivo che non inoltra pacchetti da altri dispositivi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "他のデバイスからのパケットを転送しないデバイス。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9289,6 +11015,12 @@ "value" : "Dispositivo che trasmette solo quando è necessario, per non dare nell'occhio o per risparmiare energia." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ステルス性や省電力のために必要な時にのみブロードキャストするデバイス。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9329,6 +11061,12 @@ "value" : "Diluizione della precisione (DOP) PDOP utilizzato per impostazione predefinita" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "精度希釈(DOP)、デフォルトでPDOPを使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9357,6 +11095,12 @@ "value" : "Diretto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9385,6 +11129,12 @@ "value" : "Aiuto per i messaggi diretti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージヘルプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9405,6 +11155,16 @@ } } }, + "Direct Message Key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージキー" + } + } + } + }, "Direct Messages" : { "localizations" : { "de" : { @@ -9431,6 +11191,12 @@ "value" : "Messaggi diretti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9471,6 +11237,12 @@ "value" : "I messaggi diretti utilizzano la nuova infrastruttura a chiave pubblica per la crittografia. Richiede la versione firmware 2.5 o superiore." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージは暗号化のために新しい公開鍵インフラストラクチャを使用しています。ファームウェアバージョン2.5以上が必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9499,6 +11271,12 @@ "value" : "I messaggi diretti utilizzano la chiave condivisa del canale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダイレクトメッセージはチャンネルの共有キーを使用しています。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9545,6 +11323,12 @@ "value" : "Disattivato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無効" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9603,6 +11387,12 @@ "value" : "Disconnessione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "切断" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9635,6 +11425,26 @@ } } }, + "Disconnect Node" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを切断" + } + } + } + }, + "Disconnect the currently connected node" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在接続中のノードを切断します" + } + } + } + }, "Dismiss" : { "localizations" : { "de" : { @@ -9661,6 +11471,12 @@ "value" : "Sospendere" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "閉じる" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9719,6 +11535,12 @@ "value" : "Display" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9771,6 +11593,12 @@ "value" : "Configurazione del display" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9829,6 +11657,12 @@ "value" : "Visualizzazione della configurazione ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイ設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -9869,6 +11703,12 @@ "value" : "Display Fahrenheit" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "華氏表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9897,6 +11737,12 @@ "value" : "Modalità di visualizzazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示モード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9925,6 +11771,12 @@ "value" : "Unità di visualizzazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示単位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9959,6 +11811,12 @@ "value" : "Distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "距離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -9987,6 +11845,12 @@ "value" : "Documentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドキュメント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10007,6 +11871,22 @@ } } }, + "Done" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + } + } + }, "Double Tap as Button" : { "localizations" : { "it" : { @@ -10015,6 +11895,12 @@ "value" : "Doppio tocco come pulsante" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダブルタップをボタンとして使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10061,6 +11947,12 @@ "value" : "In basso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "下" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10101,6 +11993,12 @@ "value" : "Downlink abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダウンリンク有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10129,6 +12027,12 @@ "value" : "Aggiornamento del firmware con il drag & drop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグ&ドロップファームウェア更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10157,6 +12061,12 @@ "value" : "Documentazione sull'aggiornamento del firmware con il drag & drop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグ&ドロップファームウェア更新ドキュメント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10185,6 +12095,12 @@ "value" : "Il Drag & Drop è il metodo consigliato per aggiornare il firmware dei dispositivi NRF. Se l'iPhone o l'iPad è USB-C funzionerà con il normale cavo di ricarica USB-C, mentre per i dispositivi lightning è necessario l'adattatore Apple Lightning to USB camera." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドラッグ&ドロップはNRFデバイスのファームウェア更新に推奨される方法です。お使いのiPhoneまたはiPadがUSB-Cの場合、通常のUSB-C充電ケーブルで動作します。Lightningデバイスの場合は、Apple Lightning to USBカメラアダプターが必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10205,40 +12121,6 @@ } } }, - "driving" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "fahren" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "guida" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "вожња" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "驾驶" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "開車" - } - } - } - }, "Driving" : { "localizations" : { "de" : { @@ -10253,6 +12135,12 @@ "value" : "Guida" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "運転" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10281,6 +12169,12 @@ "value" : "Spillo in Mappe" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップにピンを配置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10315,6 +12209,12 @@ "value" : "Eco" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エコー" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -10349,6 +12249,12 @@ "value" : "Modifica di Waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイント編集" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10389,6 +12295,12 @@ "value" : "Diciotto ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "18時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10429,6 +12341,12 @@ "value" : "Elev. Guadagno" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "標高ゲイン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10451,6 +12369,12 @@ "value" : "Emoji" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "絵文字" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10479,6 +12403,12 @@ "value" : "Vuoto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "空" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10507,6 +12437,12 @@ "value" : "Abilita la trasmissione di pacchetti via UDP sulla rete locale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワーク上でUDP経由のパケットブロードキャストを有効にします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10526,6 +12462,12 @@ "value" : "Abilita le notifiche" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知を有効にする" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10554,6 +12496,12 @@ "value" : "Abilita questo dispositivo come server Store and Forward. Richiede un dispositivo ESP32 con PSRAM." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスをStore and Forwardサーバーとして有効にします。PSRAMを搭載したESP32デバイスが必要です。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10588,6 +12536,12 @@ "value" : "Abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10646,6 +12600,12 @@ "value" : "Abilita le trasmissioni automatiche di TAK PLI e riduce le trasmissioni di routine." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動TAK PLIブロードキャストを有効にし、定期ブロードキャストを削減します。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10686,6 +12646,12 @@ "value" : "Consente ai dispositivi con uscita audio I2S nativa di utilizzare l'RTTTL tramite altoparlante come un cicalino. T-Watch S3 e T-Deck, ad esempio, dispongono di questa funzionalità." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネイティブI2Sオーディオ出力を持つデバイスで、ブザーのようにスピーカー経由でRTTTLを使用できるようにします。例えば、T-Watch S3やT-Deckにはこの機能があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10714,6 +12680,12 @@ "value" : "Abilita il modulo del sensore di rilevamento; deve essere abilitato sia sul nodo con il sensore, sia su tutti i nodi che si desidera ricevere messaggi di testo del sensore di rilevamento o visualizzare il registro e il grafico del sensore di rilevamento." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出センサーモジュールを有効にします。センサーを持つノードと、検出センサーテキストメッセージを受信したり、検出センサーログやチャートを表示したいノードの両方で有効にする必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10736,6 +12708,18 @@ }, "Enables the store and forward module." : { "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilita il modulo Salva & Inoltra." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Store and Forwardモジュールを有効にします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -10744,58 +12728,34 @@ } } }, - "Enabling Ethernet will disable the bluetooth connection to the app." : { + "Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "Abilitando l'Ethernet si disabilita la connessione bluetooth all'applicazione." + "value" : "Abilitando l'Ethernet verrà disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." } }, - "sr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Омогућавање етернета ће онемогућити блутут везу са апликацијом." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用以太网将禁用应用程序的蓝牙连接。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "啟用乙太網路後,將會停用與應用程式的藍牙連線。" + "value" : "Ethernetを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" } } } }, - "Enabling WiFi will disable the bluetooth connection to the app." : { + "Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "L'attivazione del WiFi disabilita la connessione bluetooth all'applicazione." + "value" : "L'attivazione del WiFi disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." } }, - "sr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Омогућавање ВајФаја ће онемогућити блутут везу са апликацијом." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用 WiFi 将禁用应用程序的蓝牙连接。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "啟用 Wi-Fi 後,將會停用與應用程式的藍牙連線。" + "value" : "WiFiを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" } } } @@ -10808,6 +12768,12 @@ "value" : "Evento di pressione dell'encoder" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エンコーダープレスイベント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10848,6 +12814,12 @@ "value" : "Crittografato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "暗号化済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -10894,6 +12866,12 @@ "value" : "Invio crittografato fallito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "暗号化送信失敗" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10916,6 +12894,12 @@ "value" : "Crittografia abilitata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "暗号化有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10950,6 +12934,12 @@ "value" : "Entrare in modalità DFU" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "DFUモードに入る" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -10978,6 +12968,12 @@ "value" : "ambiente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11012,6 +13008,12 @@ "value" : "Ambiente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11040,6 +13042,12 @@ "value" : "Metriche dei sensori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11068,6 +13076,12 @@ "value" : "Registro delle metriche ambientali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクスログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11102,6 +13116,12 @@ "value" : "Cancellare tutti i dati delle app?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全てのアプリデータを消去しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11136,6 +13156,12 @@ "value" : "Cancellare tutti i dati del dispositivo e delle app?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全てのデバイスおよびアプリデータを消去しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11164,6 +13190,12 @@ "value" : "Errore: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エラー: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11192,6 +13224,12 @@ "value" : "L'aggiornamento OTA di ESP 32 è in corso, fare clic sul pulsante qui sotto per inviare al dispositivo un messaggio di riavvio in amministrazione ota." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ESP32 OTAアップデートは開発中です。下のボタンをクリックして、デバイスにOTA管理モードへの再起動メッセージを送信してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11207,7 +13245,7 @@ "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "ESP32 的 OTA 更新功能尚在開發中,請點擊下方按鈕以傳送重新啟動至 OTA 管理模式的訊息至您的裝置。" + "value" : "ESP32 の OTA 更新功能尚在開發中,請點擊下方按鈕以傳送重新啟動至 OTA 管理模式的訊息至您的裝置。" } } } @@ -11220,6 +13258,12 @@ "value" : "Aggiornamento del firmware del dispositivo ESP32" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ESP32デバイスファームウェア更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11248,6 +13292,12 @@ "value" : "Opzioni Ethernet" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "イーサネットオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11270,6 +13320,12 @@ "value" : "Unione Europea 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "欧州連合 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11292,6 +13348,12 @@ "value" : "Unione Europea 868MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "欧州連合 868MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11320,6 +13382,12 @@ "value" : "Sera" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "夕方" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11342,6 +13410,12 @@ "value" : "Scambio di posizioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置交換" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11382,6 +13456,12 @@ "value" : "Esclamativo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "感嘆符" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11414,6 +13494,16 @@ } } }, + "Expiration" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効期限" + } + } + } + }, "Expire" : { "localizations" : { "it" : { @@ -11422,6 +13512,12 @@ "value" : "Scadenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "期限切れ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11444,6 +13540,12 @@ "value" : "Scadenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効期限" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11466,6 +13568,12 @@ "value" : "Scadenza: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効期限: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11488,6 +13596,12 @@ "value" : "Esportazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "エクスポート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11528,6 +13642,12 @@ "value" : "Notifica esterna" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11586,6 +13706,12 @@ "value" : "Configurazione della notifica esterna" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11638,6 +13764,12 @@ "value" : "Configurazione del modulo di notifica esterno ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知モジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -11684,6 +13816,12 @@ "value" : "Reset di fabbrica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Factory リセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11698,30 +13836,18 @@ } } }, - "Factory reset your device and app? " : { + "Factory reset will delete device and app data." : { "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gerät und App auf Werkseinstellungen zurücksetzen?" - } - }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Resettare il dispositivo e l'applicazione? " + "value" : "Il ripristino alle impostazioni di fabbrica eliminerà i dati del dispositivo e della app." } }, - "sr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Вратите уређај и апликацију на фабричка подешавања?" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "要將您的裝置與應用程式恢復原廠設定嗎?" + "value" : "工場出荷時リセットによりデバイスとアプリのデータが削除されます。" } } } @@ -11734,6 +13860,12 @@ "value" : "Impossibile codificare il contenuto del messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ内容のエンコードに失敗しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11756,6 +13888,12 @@ "value" : "Impossibile ottenere una posizione valida per lo scambio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "交換用の有効な位置の取得に失敗しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11778,6 +13916,12 @@ "value" : "Impossibile ottenere una posizione valida per lo scambio." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "交換用の有効な位置の取得に失敗しました。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11803,7 +13947,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Fiera" + "value" : "Discreto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "普通" } }, "sr" : { @@ -11834,6 +13984,12 @@ "value" : "Preferito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入り" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11848,6 +14004,16 @@ } } }, + "Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りと無視されたノードは常に保持されます。PKCキーを持たないノードは、ユーザーが設定したスケジュールでアプリデータベースからクリアされ、PKCキーを持つノードは間隔が7日以上に設定されている場合のみクリアされます。この機能は、デバイスノードデータベースに保存されていないノードのみをアプリから削除します。" + } + } + } + }, "Favorites" : { "localizations" : { "de" : { @@ -11862,6 +14028,12 @@ "value" : "Preferiti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入り" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11884,6 +14056,12 @@ "value" : "I preferiti e i nodi con messaggi recenti appaiono in cima all'elenco dei contatti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りと最近のメッセージがあるノードは、連絡先リストの上部に表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11918,6 +14096,12 @@ "value" : "Recuperare l'ultima posizione di un nodo cetaneo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特定のノードの最新位置を取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11940,6 +14124,12 @@ "value" : "Quindici minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "15分" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -11980,6 +14170,12 @@ "value" : "Quindici secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "15秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12020,6 +14216,12 @@ "value" : "Archiviazione dei file" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルストレージ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12048,6 +14250,12 @@ "value" : "Trova un contatto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先を検索" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12076,6 +14284,12 @@ "value" : "Trovare un nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを検索" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12116,6 +14330,12 @@ "value" : "Fine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12162,6 +14382,12 @@ "value" : "Firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12190,6 +14416,12 @@ "value" : "Documentazione sull'aggiornamento del firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェア更新ドキュメント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12224,6 +14456,12 @@ "value" : "Aggiornamenti del firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェア更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12270,6 +14508,12 @@ "value" : "Versione del firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアバージョン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12310,6 +14554,12 @@ "value" : "Sentito per la prima volta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "初回受信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12350,6 +14600,12 @@ "value" : "Cinque ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12396,6 +14652,12 @@ "value" : "Cinque minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5分" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12436,6 +14698,12 @@ "value" : "Cinque secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12494,6 +14762,12 @@ "value" : "PIN fisso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定ピン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12534,6 +14808,12 @@ "value" : "Posizione fissa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "固定位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12556,6 +14836,12 @@ "value" : "Schermo ribaltabile" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面反転" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12578,6 +14864,12 @@ "value" : "Capovolgere lo schermo in verticale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面を垂直に反転" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12615,7 +14907,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seguire" + "value" : "Segui" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "追従" } }, "pl" : { @@ -12673,7 +14971,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seguire con l'intestazione" + "value" : "Seguire la direzione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "方位で追従" } }, "pl" : { @@ -12716,6 +15020,12 @@ "value" : "Per tutte le funzionalità Mqtt diverse dal rapporto sulle mappe, è necessario impostare anche l'uplink e il downlink per ogni canale che si desidera collegare tramite Mqtt." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップレポート以外のすべてのMqtt機能については、Mqtt経由でブリッジしたい各チャンネルのアップリンクとダウンリンクも設定する必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12750,6 +15060,12 @@ "value" : "Per tutti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべての人に" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12778,6 +15094,12 @@ "value" : "Per me" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分に" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -12818,6 +15140,12 @@ "value" : "Quarantotto ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "48時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12876,6 +15204,12 @@ "value" : "Quarantacinque secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "45秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12934,6 +15268,12 @@ "value" : "Quattro ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "4時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12992,6 +15332,12 @@ "value" : "Quattro secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "4秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13038,6 +15384,12 @@ "value" : "Frequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "周波数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13060,6 +15412,12 @@ "value" : "Override di frequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "周波数オーバーライド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13082,6 +15440,12 @@ "value" : "Slot di frequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "周波数スロット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13101,7 +15465,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nome amichevole" + "value" : "Nome semplificato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フレンドリー名" } }, "sr" : { @@ -13132,6 +15502,12 @@ "value" : "Nome amichevole usato per formattare il messaggio inviato alla rete. Esempio: Il nome \"Movimento\" si tradurrebbe nel messaggio \"Movimento rilevato\"" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュに送信されるメッセージのフォーマットに使用されるフレンドリ名。例:「Motion」という名前は「Motion detected」というメッセージになります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13160,6 +15536,12 @@ "value" : "Supporto completo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完全サポート" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -13168,6 +15550,16 @@ } } }, + "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在使用中のプライベートキーを置き換える新しいプライベートキーを生成します。パブリックキーはプライベートキーから自動的に再生成されます。" + } + } + } + }, "Generate QR Code" : { "localizations" : { "de" : { @@ -13194,6 +15586,12 @@ "value" : "Generare un codice QR" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコード生成" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13226,6 +15624,16 @@ } } }, + "Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パブリックキーから生成され、メッシュ上の他のノードに送信されて、共有秘密キーの計算を可能にします。" + } + } + } + }, "Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets." : { "localizations" : { "it" : { @@ -13234,6 +15642,12 @@ "value" : "Nodi router con sensori solari e di rilevamento personalizzati e impermeabili, nodi da tavolo in alluminio e portatili robusti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カスタム防水ソーラー・検出センサールーターノード、アルミニウムデスクトップノード、頑丈なハンドセットを入手できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13262,6 +15676,12 @@ "value" : "Ottenere la posizione del nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード位置取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13290,6 +15710,12 @@ "value" : "Scarica NRF DFU dall'App Store" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "App StoreからNRF DFUを取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13321,6 +15747,12 @@ "value" : "Ottenere l'ultimo firmware alfa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新のアルファ版ファームウェアを取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13349,6 +15781,12 @@ "value" : "Ottenere l'ultimo firmware stabile" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新の安定版ファームウェアを取得" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13369,6 +15807,16 @@ } } }, + "GitHub Repository" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHubリポジトリ" + } + } + } + }, "Good" : { "localizations" : { "it" : { @@ -13377,6 +15825,12 @@ "value" : "Buono" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "良好" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13399,6 +15853,12 @@ "value" : "GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13427,6 +15887,12 @@ "value" : "Durata dell'uscita GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO出力時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13449,6 +15915,12 @@ "value" : "Pin GPIO per la porta A dell'encoder rotativo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリーエンコーダーAポート用GPIOピン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13471,6 +15943,12 @@ "value" : "Pin GPIO per la porta B dell'encoder rotativo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリーエンコーダーBポート用GPIOピン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13493,6 +15971,12 @@ "value" : "Pin GPIO per encoder rotativo Porta di stampa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリーエンコーダープレスポート用GPIOピン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13515,6 +15999,12 @@ "value" : "Pin GPIO da monitorare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIO ピン to monitor" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13537,6 +16027,12 @@ "value" : "GPS IT GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS有効化GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13565,6 +16061,12 @@ "value" : "Formato GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS形式" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13593,6 +16095,12 @@ "value" : "Ricezione GPS GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS受信GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13621,6 +16129,12 @@ "value" : "Trasmissione GPS GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS送信GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13655,6 +16169,12 @@ "value" : "Messaggio di gruppo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループメッセージ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13683,6 +16203,12 @@ "value" : "Raffiche %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "突風 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13705,6 +16231,12 @@ "value" : "חחח" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハハ" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -13730,6 +16262,9 @@ } } } + }, + "Hard Reset" : { + }, "Hardware" : { "localizations" : { @@ -13739,6 +16274,12 @@ "value" : "Hardware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハードウェア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13767,6 +16308,12 @@ "value" : "Pericoloso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "危険" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13786,7 +16333,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Intestazione" + "value" : "Direzione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "方位" } }, "sr" : { @@ -13808,7 +16361,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Intestazione: %@" + "value" : "Direzione: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "方位: %@" } }, "sr" : { @@ -13851,6 +16410,12 @@ "value" : "Ascoltato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信済み" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13909,6 +16474,12 @@ "value" : "Cuore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハート" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13941,34 +16512,6 @@ } } }, - "Help with App Development" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aiuto per lo sviluppo di app" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Помози при развоју апликације" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "帮助开发应用程序" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "幫助App開發" - } - } - } - }, "Hide alerts" : { "localizations" : { "it" : { @@ -13977,6 +16520,12 @@ "value" : "Nascondere gli avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラートを非表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -13999,6 +16548,12 @@ "value" : "Nascondi avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラートを非表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14013,6 +16568,16 @@ } } }, + "Hide sidebar" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サイドバーを隠す" + } + } + } + }, "HIGH" : { "localizations" : { "de" : { @@ -14027,6 +16592,12 @@ "value" : "ALTO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14047,40 +16618,6 @@ } } }, - "hiking" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "wandern" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "escursione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "планинарње" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "徒步" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "登山" - } - } - } - }, "Hiking" : { "localizations" : { "de" : { @@ -14095,6 +16632,12 @@ "value" : "Escursioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハイキング" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14123,6 +16666,12 @@ "value" : "Storia Rendimento Max" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "履歴返信最大数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14145,6 +16694,12 @@ "value" : "Finestra di restituzione della cronologia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "履歴返信時間枠" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14170,7 +16725,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Via il luppolo" + "value" : "Distanza in Hop" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離" } }, "sr" : { @@ -14201,6 +16762,12 @@ "value" : "Luppolo lontano %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離 %d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14229,6 +16796,12 @@ "value" : "Via il luppolo:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14257,6 +16830,12 @@ "value" : "Luppolo in partenza: %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ距離: %d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14285,6 +16864,12 @@ "value" : "Ora" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14313,6 +16898,12 @@ "value" : "Ciclo di lavoro orario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時間あたりデューティサイクル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14335,6 +16926,12 @@ "value" : "Per quanto tempo lo schermo rimane acceso dopo la pressione del tasto utente o la ricezione di messaggi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーボタンが押されたり、メッセージが受信された後、画面が点灯し続ける時間。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14363,6 +16960,12 @@ "value" : "Con quale frequenza vengono inviate le metriche del dispositivo attraverso la rete. L'impostazione predefinita è 30 minuti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクスがメッシュ経由で送信される頻度。デフォルトは30分です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14391,6 +16994,12 @@ "value" : "Con quale frequenza vengono inviate le metriche dei sensori sulla rete. L'impostazione predefinita è 30 minuti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクスがメッシュ経由で送信される頻度。デフォルトは30分です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14419,6 +17028,12 @@ "value" : "Con quale frequenza vengono inviate le metriche di potenza attraverso la rete. L'impostazione predefinita è 30 minuti." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスがメッシュ経由で送信される頻度。デフォルトは30分です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14447,6 +17062,12 @@ "value" : "Con quale frequenza dobbiamo cercare di ottenere una posizione GPS." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPS位置を取得する頻度。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14475,6 +17096,12 @@ "value" : "Con quale frequenza inviare lo stato del sensore di rilevamento alla rete, indipendentemente dal rilevamento. L'impostazione predefinita è Mai." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出の有無に関係なく、検出センサーの状態をメッシュに送信する頻度。デフォルトは「なし」です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14515,6 +17142,12 @@ "value" : "Quanto spesso possiamo inviare un messaggio alla rete quando le persone vengono rilevate." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "人が検出された時にメッシュにメッセージを送信する頻度。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -14561,6 +17194,12 @@ "value" : "Come aggiornare il firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアの更新方法" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14589,6 +17228,12 @@ "value" : "Um" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "湿度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14617,6 +17262,12 @@ "value" : "Umidità" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "湿度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14651,6 +17302,12 @@ "value" : "Ibrido" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハイブリッド" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -14703,6 +17360,12 @@ "value" : "Flyover ibrido" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハイブリッド・フライオーバー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -14737,6 +17400,12 @@ }, "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上記を読み理解しました。MQTT経由でのノードデータの暗号化されない送信に自発的に同意します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -14753,6 +17422,12 @@ "value" : "IAQ" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14781,6 +17456,12 @@ "value" : "IAQ " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ " + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14809,6 +17490,12 @@ "value" : "IAQ %lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAQ %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14837,6 +17524,12 @@ "value" : "Icona" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アイコン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14865,6 +17558,12 @@ "value" : "Se è impostato DOP, utilizzare i valori HDOP / VDOP invece di PDOP" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "DOPが設定されている場合、PDOPの代わりにHDOP / VDOP値を使用します" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14893,6 +17592,12 @@ "value" : "Se abilitato, il pin di 'uscita' sarà tirato attivo alto, mentre se disabilitato significa attivo basso." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効にすると、「出力」ピンがアクティブハイになり、無効にするとアクティブローになります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14921,6 +17626,12 @@ "value" : "Se è difficile accedere al pulsante di ripristino del dispositivo, accedere alla modalità DFU." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスのリセットボタンにアクセスが困難な場合は、ここでDFUモードに入ってください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14949,6 +17660,12 @@ "value" : "Se è impostata, i pacchetti inviati saranno ritrasmessi al dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定すると、送信したパケットがデバイスにエコーバックされます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -14977,6 +17694,12 @@ "value" : "Se l'argomento predefinito della regione è troppo frequentato, è possibile scegliere un argomento più locale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デフォルトの地域トピックが混雑している場合は、よりローカルなトピックを選択できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15005,6 +17728,12 @@ "value" : "Ignorare MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTを無視" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15033,6 +17762,12 @@ "value" : "Ignorare il nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを無視" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15061,6 +17796,12 @@ "value" : "Ignorato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無視" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15083,6 +17824,12 @@ }, "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Local Onlyのように外部メッシュからの観測メッセージを無視しますが、さらに進んで、ノードの既知リストにないノードからのメッセージも無視します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15093,6 +17840,12 @@ }, "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンまたは復号化できない外部メッシュからの観測メッセージを無視します。ノードのローカル主要/副次チャンネルでのみメッセージを再ブロードキャストします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15109,6 +17862,12 @@ "value" : "Percorso di importazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートインポート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15128,6 +17887,9 @@ } } } + }, + "In addition to Config, Keys and BLE bonds will be wiped" : { + }, "Include" : { "localizations" : { @@ -15155,6 +17917,12 @@ "value" : "Includere" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "含める" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15187,7 +17955,7 @@ } } }, - "incomplete" : { + "Incomplete" : { "localizations" : { "de" : { "stringUnit" : { @@ -15207,6 +17975,12 @@ "value" : "Incompleto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未完了" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15223,6 +17997,12 @@ }, "India" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15245,6 +18025,12 @@ "value" : "Qualità dell'aria interna" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "室内空気品質" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15273,6 +18059,12 @@ "value" : "Qualità dell'aria interna (IAQ)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "室内空気質(IAQ)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15319,6 +18111,12 @@ "value" : "Nodo infrastrutturale solo su una torre o sulla cima di una montagna. Non deve essere utilizzato per tetti o nodi mobili. Necessita di una copertura eccezionale. Visibile nell'elenco dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タワーまたは山頂のみのインフラストラクチャーノード。屋根や移動ノードには使用しないでください。優れたカバレッジが必要です。ノードリストに表示されます。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15377,6 +18175,12 @@ "value" : "Nodo infrastrutturale solo su una torre o sulla cima di una montagna. Non deve essere utilizzato per tetti o nodi mobili. Trasmette i messaggi con un overhead minimo. Non visibile nell'elenco dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タワーまたは山頂のみのインフラストラクチャーノード。屋根や移動ノードには使用しないでください。最小限のオーバーヘッドでメッセージを中継します。ノードリストには表示されません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15423,6 +18227,12 @@ "value" : "Nodo infrastruttura che ritrasmette sempre i pacchetti una volta, ma solo dopo tutte le altre modalità, garantendo una copertura aggiuntiva per i cluster locali. Visibile nell'elenco dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常にパケットを一度だけ再ブロードキャストするインフラストラクチャーノードですが、他のすべてのモードの後にのみ実行し、ローカルクラスターの追加カバレッジを確保します。ノードリストに表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15445,6 +18255,12 @@ "value" : "Ingressi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "入力" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15473,6 +18289,12 @@ "value" : "Barra superiore invertita per la visualizzazione a 2 colori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2色ディスプレイ用反転トップバー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15513,6 +18335,12 @@ "value" : "Emissione di Want Config a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ に設定要求を送信中" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15553,6 +18381,12 @@ "value" : "Giappone" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日本" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15575,6 +18409,12 @@ "value" : "JSON abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15603,6 +18443,12 @@ "value" : "La modalità JSON è un output MQTT limitato e non criptato per l'integrazione locale con l'assistente domestico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSONモードは、Home Assistantとのローカル統合のための限定的で暗号化されていないMQTT出力です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15625,6 +18471,12 @@ }, "Jump to present" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新に移動" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -15647,6 +18499,12 @@ "value" : "Chiave" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15667,6 +18525,16 @@ } } }, + "Key Backup" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーバックアップ" + } + } + } + }, "Key Mapping" : { "localizations" : { "it" : { @@ -15675,6 +18543,12 @@ "value" : "Mappatura delle chiavi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーマッピング" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15703,6 +18577,12 @@ "value" : "Dimensione della chiave" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーサイズ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15725,6 +18605,12 @@ "value" : "Corea" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "韓国" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15753,6 +18639,12 @@ "value" : "L'ultima volta che si è sentito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最終受信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15787,6 +18679,12 @@ "value" : "Latitudine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緯度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15807,6 +18705,26 @@ } } }, + "Latitude in degrees (e.g., 37.7749)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緯度(度単位、例: 37.7749)" + } + } + } + }, + "Latitude must be between -90 and 90 degrees" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "緯度は-90度から90度の間である必要があります" + } + } + } + }, "LED Heartbeat" : { "localizations" : { "it" : { @@ -15815,6 +18733,12 @@ "value" : "Battito cardiaco a LED" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LEDハートビート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15843,6 +18767,12 @@ "value" : "Stato del LED" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LED状態" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -15889,6 +18819,12 @@ "value" : "A sinistra" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "左" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15921,34 +18857,6 @@ } } }, - "Legacy Administration" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alte Administrationsart" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Amministrazione del patrimonio" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стари начин администрације" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "舊版遠端管理" - } - } - } - }, "Level" : { "localizations" : { "de" : { @@ -15975,6 +18883,12 @@ "value" : "Livello" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レベル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16015,6 +18929,12 @@ "value" : "Operatore con licenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンスオペレーター" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16043,6 +18963,12 @@ "value" : "Limitare tutti gli intervalli di trasmissione periodica, in particolare la telemetria e la posizione. Se è necessario aumentare gli hop, farlo sui nodi ai margini, non su quelli al centro. MQTT è sconsigliato quando il ciclo di lavoro è limitato, perché è il nodo gateway a svolgere tutto il lavoro." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特にテレメトリと位置情報のすべての定期ブロードキャスト間隔を制限します。ホップを増やす必要がある場合は、中央のノードではなく端のノードで行ってください。デューティサイクルが制限されている場合、ゲートウェイノードがすべての作業を行うため、MQTTは推奨されません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16071,6 +18997,12 @@ "value" : "Serie di linee" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "線系列" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16093,6 +19025,12 @@ "value" : "Caricamento dei log. . ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loading ログs. . ." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16113,40 +19051,6 @@ } } }, - "Location" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posizione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Локација:" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "位置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "位置" - } - } - } - }, "Location:" : { "localizations" : { "de" : { @@ -16161,6 +19065,12 @@ "value" : "Posizione:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "場所:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16195,6 +19105,12 @@ "value" : "Bloccato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロック済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16223,6 +19139,12 @@ "value" : "Livelli del registro" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログレベル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16269,6 +19191,12 @@ "value" : "Registrazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログ記録" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16309,6 +19237,12 @@ "value" : "Registri" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16337,6 +19271,12 @@ "value" : "Registri:" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログ:" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16371,6 +19311,12 @@ "value" : "Nome lungo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長い名前" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16391,40 +19337,6 @@ } } }, - "Long Name: %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Langer Name: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome lungo: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дуго име: %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "长名称: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "完整名稱:%@" - } - } - } - }, "Long press to favorite or mute the contact or delete a conversation." : { "localizations" : { "it" : { @@ -16433,6 +19345,12 @@ "value" : "Premere a lungo per privilegiare o silenziare il contatto o eliminare una conversazione." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長押しで連絡先をお気に入りに追加、ミュート、または会話を削除できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16461,6 +19379,12 @@ "value" : "A lungo raggio - Veloce" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長距離 - 高速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16483,6 +19407,12 @@ "value" : "Lungo raggio - Moderato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長距離 - 中程度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16505,6 +19435,12 @@ "value" : "Lungo raggio - Lento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "長距離 - 低速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16533,6 +19469,12 @@ "value" : "Longitudine" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "経度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16553,6 +19495,26 @@ } } }, + "Longitude in degrees (e.g., -122.4194)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "経度(度単位、例: -122.4194)" + } + } + } + }, + "Longitude must be between -180 and 180 degrees" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "経度は-180度から180度の間である必要があります" + } + } + } + }, "LoRa" : { "localizations" : { "de" : { @@ -16579,6 +19541,12 @@ "value" : "LoRa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16637,6 +19605,12 @@ "value" : "Configurazione LoRa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16668,6 +19642,9 @@ } } } + }, + "LoRa Config Changes:" : { + }, "LoRa config received: %@" : { "localizations" : { @@ -16695,6 +19672,12 @@ "value" : "Configurazione LoRa ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16741,6 +19724,12 @@ "value" : "Oggetti smarriti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "落とし物" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16769,6 +19758,12 @@ "value" : "BASSO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "低" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16803,6 +19798,12 @@ "value" : "M5 Stack Card KB / Tastiera RAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "M5 Stack Card KB / RAK キーパッド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16831,6 +19832,12 @@ "value" : "Malesia 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マレーシア 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16853,6 +19860,12 @@ "value" : "Malesia 919MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マレーシア 919MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16893,6 +19906,12 @@ "value" : "Gestire i canali" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル管理" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -16933,6 +19952,12 @@ "value" : "Dispositivo gestito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理されたデバイス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -16979,6 +20004,12 @@ "value" : "Configurazione manuale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手動設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17025,6 +20056,12 @@ "value" : "Opzioni mappa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17053,6 +20090,12 @@ "value" : "Intervallo di pubblicazione della mappa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップ公開間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17081,6 +20124,12 @@ "value" : "Rapporto sulla mappa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップレポート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17127,6 +20176,12 @@ "value" : "Raggiunta la massima ritrasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大再送信回数に到達" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17167,6 +20222,12 @@ "value" : "Medio raggio - Veloce" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中距離 - 高速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17189,6 +20250,12 @@ "value" : "Medio raggio - Lento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中距離 - 低速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17211,6 +20278,12 @@ "value" : "Aggiornamento dell'attività di rete" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュアクティビティ更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17251,6 +20324,12 @@ "value" : "Rete Attività live" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュライブアクティビティ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17309,6 +20388,12 @@ "value" : "Mappa della mesh" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュマップ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17358,6 +20443,12 @@ "value" : "Il Nodo Meshtastic %@ ha condiviso i canali con voi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticノード %@ があなたとチャンネルを共有しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17383,6 +20474,12 @@ "value" : "Meshtastic® Copyright Meshtastic LLC" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic® Copyright Meshtastic LLC" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17396,9 +20493,6 @@ } } } - }, - "message" : { - }, "Message" : { "localizations" : { @@ -17414,6 +20508,12 @@ "value" : "Messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17442,6 +20542,12 @@ "value" : "Il contenuto del messaggio supera i 200 byte." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ内容が200バイトを超えています。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17482,6 +20588,12 @@ "value" : "Dettagli del messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ詳細" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17540,6 +20652,12 @@ "value" : "Messaggio ricevuto dall'app messaggi di testo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストメッセージアプリからメッセージを受信しました。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17598,6 +20716,12 @@ "value" : "Invio messaggio fallito, connessione non corretta a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ送信に失敗しました。%@ に適切に接続されていません" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17630,6 +20754,17 @@ } } }, + "Message Size" : { + "comment" : "VoiceOver label for message size", + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージサイズ" + } + } + } + }, "Message Status Options" : { "localizations" : { "it" : { @@ -17638,6 +20773,12 @@ "value" : "Opzioni di stato del messaggio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ Status Options" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17672,6 +20813,12 @@ "value" : "Messaggi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17718,6 +20865,12 @@ "value" : "I messaggi sono separati da |" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージs separate with |" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17732,6 +20885,16 @@ } } }, + "Messaging" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージング" + } + } + } + }, "Metric" : { "localizations" : { "it" : { @@ -17740,6 +20903,12 @@ "value" : "Metrico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メトリック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17768,6 +20937,12 @@ "value" : "Mezzogiorno" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "正午" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17808,6 +20983,12 @@ "value" : "Sistema di riferimento della griglia militare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "軍用格子座標系" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17854,6 +21035,12 @@ "value" : "Distanza minima" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最小距離" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17882,6 +21069,12 @@ "value" : "Intervallo minimo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最小間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17904,6 +21097,12 @@ "value" : "Tempo minimo tra le trasmissioni di rilevamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出ブロードキャスト間の最小時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17926,6 +21125,12 @@ "value" : "Tempo minimo tra le trasmissioni di rilevamento. L'impostazione predefinita è 45 secondi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検出ブロードキャスト間の最小時間。デフォルトは45秒です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -17966,6 +21171,12 @@ "value" : "Modalità" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モード" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18006,6 +21217,12 @@ "value" : "Modello" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モデル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18034,6 +21251,12 @@ "value" : "Moderato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中程度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18080,6 +21303,12 @@ "value" : "Configurazione del modulo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "モジュール設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18126,6 +21355,12 @@ "value" : "Mattina" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "朝" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18163,7 +21398,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale primario. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" + "value" : "La maggior parte dei dati sulla rete viene inviata attraverso il canale principale. È possibile impostare canali secondari per creare gruppi di messaggistica aggiuntivi protetti da una propria chiave. [Suggerimenti per la configurazione del canale](https://meshtastic.org/docs/configuration/tips/)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュ上のほとんどのデータはプライマリチャンネル経由で送信されます。セカンダリチャンネルを設定して、独自のキーで保護された追加のメッセージグループを作成できます。[チャンネル設定のヒント](https://meshtastic.org/docs/configuration/tips/)" } }, "pl" : { @@ -18206,6 +21447,12 @@ "value" : "MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18252,6 +21499,12 @@ "value" : "Proxy client MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTクライアントプロキシ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18310,6 +21563,12 @@ "value" : "Configurazione MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18368,6 +21627,12 @@ "value" : "Configurazione del modulo MQTT ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18420,6 +21685,12 @@ "value" : "Moltiplicatore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "乗数" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18454,6 +21725,12 @@ "value" : "Deve essere una singola emoji" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "単一の絵文字である必要があります" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18494,6 +21771,12 @@ "value" : "MyInfo ricevuto: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マイ情報受信: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18534,6 +21817,12 @@ "value" : "Timeout di Nag" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知タイムアウト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18562,6 +21851,12 @@ "value" : "Nome" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名前" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18596,6 +21891,12 @@ "value" : "Il nome deve essere inferiore a 30 byte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "名前は30バイト未満である必要があります" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18624,6 +21925,12 @@ "value" : "Spostarsi sul nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードに移動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18646,6 +21953,12 @@ "value" : "Argomenti vicini" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "近くのトピック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18686,6 +21999,12 @@ "value" : "Rete" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18744,6 +22063,12 @@ "value" : "Configurazione della rete" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18802,6 +22127,12 @@ "value" : "Configurazione di rete ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -18842,6 +22173,12 @@ "value" : "Stato della rete Arancione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク状態 オレンジ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18870,6 +22207,12 @@ "value" : "Stato della rete Rosso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワーク状態 レッド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18904,6 +22247,12 @@ "value" : "Mai" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "なし" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18926,6 +22275,12 @@ "value" : "Nuovo nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいノード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18948,6 +22303,12 @@ "value" : "È stato scoperto un nuovo nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいノードが発見されました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18970,6 +22331,12 @@ "value" : "Nuova Zelanda 865MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ニュージーランド 865MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -18998,6 +22365,12 @@ "value" : "È disponibile un firmware più recente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "新しいファームウェアが利用可能です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19032,6 +22405,12 @@ "value" : "Notte" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "夜間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19072,6 +22451,12 @@ "value" : "Posizioni NMEA" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "NMEA位置情報" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19130,6 +22515,12 @@ "value" : "Nessun canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルなし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19176,6 +22567,12 @@ "value" : "Nessun nodo collegato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続されたノードがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19216,6 +22613,12 @@ "value" : "Nessun dispositivo collegato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスが接続されていません" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19256,6 +22659,12 @@ "value" : "Nessuna metrica del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメトリクスがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19278,6 +22687,12 @@ "value" : "Nessuna metrica ambientale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "環境メトリクスがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19324,6 +22739,12 @@ "value" : "Nessuna interfaccia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インターフェースがありません" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19364,6 +22785,12 @@ "value" : "Nessun registro del contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターログがありません" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -19410,6 +22837,12 @@ "value" : "Nessun PIN (funziona e basta)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PIN不要(自動接続)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19456,6 +22889,12 @@ "value" : "Nessuna posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報がありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19478,6 +22917,12 @@ "value" : "Nessuna metrica di potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスがありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19518,6 +22963,12 @@ "value" : "Nessuna risposta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答なし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19576,6 +23027,12 @@ "value" : "Nessun percorso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートなし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19622,6 +23079,12 @@ "value" : "Nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19650,6 +23113,12 @@ "value" : "Backup dei dati del nucleo del nodo %1$@/%2$@ - %3$@ - %4$@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードコアデータバックアップ %1$@/%2$@ - %3$@ - %4$@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19684,6 +23153,12 @@ "value" : "Il nodo non ha posizioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードに位置情報がありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19712,6 +23187,12 @@ "value" : "Storia del nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Node 履歴" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19734,6 +23215,12 @@ "value" : "Intervallo di trasmissione delle informazioni sul nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード情報ブロードキャスト間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19774,6 +23261,12 @@ "value" : "Ricevute informazioni sul nodo per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード情報を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19820,6 +23313,12 @@ "value" : "Mappa dei nodi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Node マップ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19848,6 +23347,12 @@ "value" : "Numero di nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード番号" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -19888,6 +23393,12 @@ "value" : "Nodi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -19940,6 +23451,12 @@ "value" : "Nodi (%@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード (%@)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19998,6 +23515,12 @@ "value" : "Nessuno" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "なし" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20038,6 +23561,12 @@ "value" : "Non è un file di percorso valido" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "有効なルートファイルではありません" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20078,6 +23607,12 @@ "value" : "Non autorizzato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未認証" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20130,6 +23665,12 @@ "value" : "Non presente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "存在しません" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -20170,6 +23711,12 @@ "value" : "Note" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メモ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20184,34 +23731,6 @@ } } }, - "Num: %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anzahl: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Num: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Број: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "Num: %@" - } - } - } - }, "Number of hops" : { "localizations" : { "de" : { @@ -20226,6 +23745,12 @@ "value" : "Numero di hop" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホップ数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20254,6 +23779,12 @@ "value" : "Numero di record" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レコード数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20282,6 +23813,12 @@ "value" : "Numero di satelliti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20322,6 +23859,12 @@ "value" : "Spento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オフ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20368,6 +23911,12 @@ "value" : "OK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20390,6 +23939,12 @@ "value" : "Ok a MQTT" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT OK" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20424,6 +23979,12 @@ "value" : "Tipo OLED" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OLEDタイプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20470,6 +24031,12 @@ "value" : "Solo all'avvio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "起動時のみ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20510,6 +24077,12 @@ "value" : "Per l'attivazione degli operatori con licenza è necessario il firmware 2.0.20 o superiore. Assicuratevi di consultare le normative locali e di contattare i coordinatori delle frequenze amatoriali locali per eventuali domande." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンス取得者のオンボーディングにはファームウェア2.0.20以上が必要です。必ずお住まいの地域の規制を参照し、疑問がある場合は地域のアマチュア周波数コーディネーターにお問い合わせください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20556,6 +24129,12 @@ "value" : "Un'ora" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20614,6 +24193,12 @@ "value" : "Un minuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20672,6 +24257,12 @@ "value" : "Un secondo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20718,6 +24309,12 @@ "value" : "In linea" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オンライン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20734,6 +24331,12 @@ }, "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SENSOR、TRACKER、TAK_TRACKERロールでのみ許可されており、CLIENT_MUTEロールと同様にすべての再ブロードキャストを抑制します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20744,6 +24347,12 @@ }, "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コアポート番号からのパケットのみ再ブロードキャスト: ノード情報、テキスト、位置、テレメトリ、ルーティング。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -20778,6 +24387,12 @@ "value" : "Codice di localizzazione aperto (alias Codice Plus)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンロケーションコード(プラスコード)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20824,6 +24439,12 @@ "value" : "Aprire le impostazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定を開く" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20852,6 +24473,12 @@ "value" : "Ottimizzato per i display a 2 colori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2色ディスプレイ用に最適化" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20892,6 +24519,12 @@ "value" : "Ottimizzato per la comunicazione del sistema ATAK, riduce le trasmissioni di routine." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ATAKシステム通信用に最適化、定期ブロードキャストを削減。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20932,6 +24565,12 @@ "value" : "Campi opzionali da includere quando si assemblano i messaggi di posizione. Più campi sono inclusi, più grande sarà il messaggio, con conseguente allungamento dei tempi di trasmissione e un maggiore rischio di perdita di pacchetti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置メッセージを組み立てる際に含めるオプションフィールド。含めるフィールドが多いほどメッセージが大きくなり、通信時間が長くなってパケット損失のリスクが高くなります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -20960,6 +24599,12 @@ "value" : "GPIO opzionale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプション GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21000,6 +24645,12 @@ "value" : "Opzioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オプション" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21058,6 +24709,12 @@ "value" : "Riferimento di griglia Ordnance Survey" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "英国陸地測量部格子座標" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21098,6 +24755,12 @@ "value" : "Dettagli della voce del registro OS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OSログエントリ詳細" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21120,6 +24783,12 @@ "value" : "Gli aggiornamenti OTA non sono supportati da questo dispositivo NRF." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このNRFデバイスではOTA更新はサポートされていません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21148,6 +24817,12 @@ "value" : "Gli aggiornamenti OTA non sono supportati dalla vostra piattaforma." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お使いのプラットフォームではOTA更新はサポートされていません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21176,6 +24851,12 @@ "value" : "Altre fonti di dati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他のデータソース" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21204,6 +24885,12 @@ "value" : "Emissione di registrazioni di debug in tempo reale via seriale, visualizzazione ed esportazione di registri del dispositivo con correzione della posizione via Bluetooth." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアル経由でライブデバッグログを出力し、Bluetooth経由で位置情報を削除したデバイスログを表示・エクスポート。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21226,6 +24913,12 @@ "value" : "Pin di uscita cicalino GPIO " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力ピンブザーGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21248,6 +24941,12 @@ "value" : "Pin di uscita GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力ピンGPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21270,6 +24969,12 @@ "value" : "Pin di uscita vibra GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力ピン振動GPIO" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21284,34 +24989,6 @@ } } }, - "overlanding" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "overland drive" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вожња преко копна" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "越野" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "開車 (overland drive)" - } - } - } - }, "Overlanding" : { "localizations" : { "it" : { @@ -21320,6 +24997,12 @@ "value" : "Overlanding" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オーバーランド" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21348,6 +25031,12 @@ "value" : "Annulla il rilevamento automatico dello schermo OLED." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動OLEDスクリーン検出をオーバーライド。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21385,7 +25074,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modalità di accoppiamento" + "value" : "Modalità di associazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペアリングモード" } }, "pl" : { @@ -21446,6 +25141,12 @@ "value" : "Password" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワード" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21504,6 +25205,12 @@ "value" : "Pausa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "一時停止" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21550,6 +25257,12 @@ "value" : "Contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンター" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21596,6 +25309,12 @@ "value" : "Configurazione del contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンター設定" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -21630,6 +25349,12 @@ "value" : "Configurazione del contatore PAX ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンター設定を受信しました: %@" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -21641,6 +25366,12 @@ "state" : "translated", "value" : "Конфигурација PAX бројача примљена: %@" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAX 計數器設定已收到: %@" + } } } }, @@ -21652,6 +25383,12 @@ "value" : "Registro del contatore PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターログ" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -21692,6 +25429,12 @@ "value" : "Messaggio del contatore PAX ricevuto da: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターメッセージを受信: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21725,7 +25468,20 @@ } }, "paxcounter.log" : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターログ" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "paxcounter.log" + } + } + } }, "Perform a factory reset on the node you are connected to" : { "localizations" : { @@ -21741,6 +25497,12 @@ "value" : "Eseguire un reset di fabbrica sul nodo a cui si è connessi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続しているノードの工場出荷時リセットを実行" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21763,6 +25525,12 @@ "value" : "Filippine 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィリピン 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21785,6 +25553,12 @@ "value" : "Filippine 868MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィリピン 868MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21807,6 +25581,12 @@ "value" : "Filippine 915MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィリピン 915MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21847,6 +25627,12 @@ "value" : "Telefono GPS" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマートフォンGPS" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21887,6 +25673,12 @@ "value" : "Pin %lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピン %lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21909,6 +25701,12 @@ "value" : "Pin A" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピンA" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21931,6 +25729,12 @@ "value" : "Pin B" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ピンB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21959,6 +25763,12 @@ "value" : "Amministrazione dei nodi basata su PKI, richiede la versione firmware 2.5+" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKIベースのノード管理、ファームウェアバージョン2.5+が必要" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -21981,6 +25791,12 @@ }, "Please be advised that because the map report is not encrypted, your data may be stored and displayed permanently by third parties. Meshtastic does not assume responsibility for any such storage, display or disclosure of this data." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マップレポートは暗号化されていないため、あなたのデータが第三者によって永続的に保存・表示される可能性があることをご承知ください。Meshtasticは、このようなデータの保存、表示、開示について一切の責任を負いません。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -21997,6 +25813,12 @@ "value" : "Collegarsi a una radio per configurare le impostazioni." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定を構成するには無線機に接続してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22025,6 +25847,12 @@ "value" : "Impostare una regione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地域を設定してください" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22047,6 +25875,12 @@ "value" : "Punti di interesse" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "興味地点" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22087,6 +25921,12 @@ "value" : "Cacca" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "うんち" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22133,6 +25973,12 @@ "value" : "Posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22185,6 +26031,12 @@ "value" : "Configurazione della posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22217,6 +26069,64 @@ } } }, + "Position config received: %@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Positionskonfiguration empfangen: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuration de la position reçue : %@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הגדרות מיקום התקבלו: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurazione della posizione ricevuta: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置設定を受信しました: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odebrano konfigurację pozycji: %@" + } + }, + "se" : { + "stringUnit" : { + "state" : "translated", + "value" : "Positionskonfiguration mottagen: %@" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Конфигурација позиције примљена: %@" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "收到位置設定檔: %@" + } + } + } + }, "Position Exchange Failed" : { "localizations" : { "it" : { @@ -22225,6 +26135,12 @@ "value" : "Scambio di posizioni non riuscito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置交換に失敗しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22247,6 +26163,12 @@ "value" : "Scambio di posizioni richiesto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置交換要求" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22269,6 +26191,12 @@ "value" : "Bandiere di posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置フラグ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22288,7 +26216,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Diario di posizione" + "value" : "Registro di posizione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置ログ" } }, "sr" : { @@ -22313,6 +26247,12 @@ "value" : "Posizione Log %lld Punti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置ログ %lld ポイント" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22335,6 +26275,12 @@ "value" : "Pacchetto posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置パケット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22375,6 +26321,12 @@ "value" : "Posizione Pacchetto ricevuto dal nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード %@ から位置パケットを受信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22421,6 +26373,12 @@ "value" : "Posizione inviata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置送信済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22443,6 +26401,12 @@ "value" : "Posizioni abilitate" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22471,6 +26435,12 @@ "value" : "Le posizioni saranno fornite dal GPS del dispositivo; se si seleziona disabilitato o non presente, è possibile impostare una posizione fissa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報はデバイスのGPSによって提供されます。無効または存在しないを選択した場合は、固定位置を設定できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22491,52 +26461,6 @@ } } }, - "Positon config received: %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Positionskonfiguration empfangen: %@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuration de la position reçue : %@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הגדרות מיקום התקבלו: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurazione della posizione ricevuta: %@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odebrano konfigurację pozycji: %@" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Positionskonfiguration mottagen: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Конфигурација позиције примљена: %@" - } - } - } - }, "Power" : { "localizations" : { "de" : { @@ -22557,6 +26481,12 @@ "value" : "Potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22609,6 +26539,12 @@ "value" : "Configurazione dell'alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22649,6 +26585,12 @@ "value" : "Configurazione dell'alimentazione ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源設定を受信しました: %@" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -22677,6 +26619,12 @@ "value" : "Metriche di potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源メトリクス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22699,6 +26647,12 @@ "value" : "Registro delle metriche di potenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力メトリクスログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22721,6 +26675,12 @@ "value" : "Spegnimento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源オフ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22743,6 +26703,12 @@ "value" : "Opzioni di alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源オプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22777,6 +26743,12 @@ "value" : "Risparmio energetico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "省電力" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -22817,6 +26789,12 @@ "value" : "Schermo di alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源画面" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22845,6 +26823,12 @@ "value" : "Potenziato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源供給中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22873,6 +26857,12 @@ "value" : "Posizione precisa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "精密位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22895,6 +26885,12 @@ "value" : "Preimpostazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22923,6 +26919,12 @@ "value" : "Pin a pressione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プレスピン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -22945,6 +26947,12 @@ "value" : "Pressione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "気圧" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -22976,7 +26984,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Primario" + "value" : "Principale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライマリ" } }, "pl" : { @@ -23025,6 +27039,12 @@ "value" : "Chiave amministrativa primaria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライマリ管理キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23050,7 +27070,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "GPIO primario" + "value" : "GPIO principale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライマリGPIO" } }, "sr" : { @@ -23081,6 +27107,12 @@ "value" : "Chiave privata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秘密キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23127,6 +27159,12 @@ "value" : "Processo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロセス" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23167,6 +27205,12 @@ "value" : "Informazioni sul progetto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロジェクト情報" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23213,6 +27257,12 @@ "value" : "Protobufs" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロトコルバッファ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23244,6 +27294,12 @@ } } } + }, + "Provide anonymous usage statistics and crash reports." : { + + }, + "Provide Confirmation" : { + }, "Public Key" : { "localizations" : { @@ -23259,6 +27315,12 @@ "value" : "Chiave pubblica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23287,6 +27349,12 @@ "value" : "Crittografia a chiave pubblica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開鍵暗号化" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23315,6 +27383,12 @@ "value" : "Mancata corrispondenza della chiave pubblica" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開キー不一致" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23343,6 +27417,12 @@ "value" : "Alimentato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パスワード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23383,6 +27463,12 @@ "value" : "Interrogativo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "質問" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23423,6 +27509,12 @@ "value" : "Radiazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "放射線" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23457,6 +27549,12 @@ "value" : "Configurazione radio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無線設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23497,6 +27595,12 @@ "value" : "Radio scollegata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無線切断" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23537,6 +27641,12 @@ "value" : "Modulo encoder rotativo RAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAKロータリーエンコーダー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23592,7 +27702,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Test della gamma" + "value" : "Test di portata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンジテスト" } }, "pl" : { @@ -23653,6 +27769,12 @@ "value" : "Configurazione del test di portata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンジテスト設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23711,6 +27833,12 @@ "value" : "Configurazione del modulo Range Test ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲テストモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23769,6 +27897,12 @@ "value" : "Riavvio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再起動" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23827,6 +27961,12 @@ "value" : "Riavviare il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードを再起動しますか?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23861,6 +28001,12 @@ }, "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートチャンネル上、または同じLoRaパラメータを持つ他のメッシュからの観測されたメッセージを再ブロードキャストします。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -23877,6 +28023,12 @@ "value" : "Modalità di ritrasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再ブロードキャストモード" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23905,6 +28057,12 @@ "value" : "Dati di ricezione (rxd) Pin GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信データ(RXD)GPIOピン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -23945,6 +28103,12 @@ "value" : "Ricevuto un riscontro negativo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "否定応答を受信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24003,6 +28167,12 @@ "value" : "Ricevuto Ack" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信確認" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24061,6 +28231,12 @@ "value" : "Destinatario Ack" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受信者確認" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24107,6 +28283,12 @@ "value" : "Percorso di registrazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート記録中" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24129,6 +28311,12 @@ "value" : "Aggiornare i metadati del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスメタデータを更新" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24143,6 +28331,16 @@ } } }, + "Regenerate Private Key" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートキーを再生成" + } + } + } + }, "Region" : { "localizations" : { "de" : { @@ -24157,6 +28355,12 @@ "value" : "Regione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地域" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24203,6 +28407,12 @@ "value" : "Raggiunto il limite del ciclo di lavoro regionale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "地域デューティサイクル制限に到達" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24243,6 +28453,12 @@ "value" : "Note di rilascio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リリースノート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24265,6 +28481,12 @@ "value" : "Amministrazione remota per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ のリモート管理" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24287,6 +28509,12 @@ "value" : "Amministratore legacy remoto: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートレガシー管理: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24309,6 +28537,12 @@ "value" : "Amministratore PKI remoto: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモートPKI管理: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24334,7 +28568,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere" + "value" : "Elimina" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" } }, "sr" : { @@ -24365,6 +28605,12 @@ "value" : "Rimuovi dai preferiti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りから削除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24384,7 +28630,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere da ignorato" + "value" : "Elimina da ignorati" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無視リストから削除" } }, "sr" : { @@ -24421,6 +28673,12 @@ "value" : "Ripetitore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リピーター" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24446,7 +28704,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sostituire i canali" + "value" : "Sostituisci canali" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル置換" } }, "sr" : { @@ -24489,6 +28753,12 @@ "value" : "Risposta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "返信" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24520,9 +28790,6 @@ } } } - }, - "Replying to a message" : { - }, "Request Legacy Admin: %@" : { "localizations" : { @@ -24532,6 +28799,12 @@ "value" : "Richiesta amministratore legacy: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レガシー管理者要求: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24554,6 +28827,12 @@ "value" : "Richiesta PKI Admin: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PKI管理者要求: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24588,6 +28867,12 @@ "value" : "Messaggi in scatola richiesti Messaggi del modulo per il nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード %@ の定型メッセージモジュールメッセージを要求しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24628,6 +28913,12 @@ "value" : "Richiede la presenza di un accelerometro sul dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスに加速度計が必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24656,6 +28947,12 @@ "value" : "Ripristino delle impostazioni dell'app" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ設定をリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24690,6 +28987,12 @@ "value" : "Azzeramento di NodeDB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "NodeDBをリセット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24718,6 +29021,12 @@ "value" : "Riavvio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再起動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24746,6 +29055,12 @@ "value" : "Riavviare al nodo a cui si è collegati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続しているノードを再起動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24760,6 +29075,16 @@ } } }, + "Restore" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "復元" + } + } + } + }, "Resume" : { "localizations" : { "de" : { @@ -24786,6 +29111,12 @@ "value" : "Il curriculum" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再開" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24832,6 +29163,12 @@ "value" : "Esaminare l'applicazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリをレビュー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -24878,6 +29215,12 @@ "value" : "Diritto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "右" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24936,6 +29279,12 @@ "value" : "Suoneria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "着信音" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24988,6 +29337,12 @@ "value" : "Configurazione della suoneria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "着信音設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25028,6 +29383,12 @@ "value" : "Lingua di trasferimento della suoneria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "着信音転送言語" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -25068,6 +29429,12 @@ "value" : "Ringtone Transfer Language(RTTTL) Stringa di suoneria utilizzata dai cicalini supportati nelle notifiche esterne." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "外部通知でサポートされているブザーで使用されるRingtone Transfer Language(RTTTL)着信音文字列。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25114,6 +29481,12 @@ "value" : "Ruolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25142,6 +29515,12 @@ "value" : "Ruolo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "役割: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25170,6 +29549,12 @@ "value" : "Ruoli" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "役割" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25192,6 +29577,12 @@ "value" : "Argomento radice" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートトピック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25214,6 +29605,12 @@ "value" : "Rotary 1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロータリー1" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25242,6 +29639,12 @@ "value" : "Percorso di ritorno: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート(復路): %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25264,6 +29667,12 @@ "value" : "Linee di percorso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートライン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25286,6 +29695,12 @@ "value" : "Registratore di percorso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートレコーダー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25308,6 +29723,12 @@ "value" : "Registrazione del percorso in pausa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート記録を一時停止" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25336,6 +29757,12 @@ "value" : "Percorso: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25364,6 +29791,12 @@ "value" : "Router" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルーター" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25398,6 +29831,12 @@ "value" : "Router tardivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルーター遅延" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25420,6 +29859,12 @@ "value" : "Percorsi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25460,6 +29905,12 @@ "value" : "Routing ricevuto per RequestID: %@ Ack Status: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リクエストID %@ のルーティングを受信、応答ステータス: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25500,6 +29951,12 @@ "value" : "RSSI %@ dBm" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %@ dBm" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25522,6 +29979,12 @@ "value" : "RSSI %ddB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %ddB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25544,6 +30007,12 @@ "value" : "RSSI %llddB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RSSI %llddB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25584,6 +30053,12 @@ "value" : "RTTTL Configurazione suoneria ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RTTTL着信音設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25618,6 +30093,12 @@ }, "Russia" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ロシア" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25640,6 +30121,12 @@ "value" : "Guadagno potenziato RX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RX ブーストゲイン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25656,6 +30143,12 @@ }, "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ALLと同じ動作ですが、パケットのデコードをスキップして単純に再ブロードキャストします。リピーター役割でのみ利用可能です。他の役割で設定するとALLの動作になります。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -25678,6 +30171,12 @@ "value" : "לווין" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25730,6 +30229,12 @@ "value" : "Sorvolo satellitare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星フライオーバー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25773,7 +30278,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sats" + "value" : "Sat" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星" } }, "sr" : { @@ -25801,7 +30312,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Stima Sats %lld" + "value" : "Stima satelliti %lld" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "衛星推定数 %lld" } }, "sr" : { @@ -25829,7 +30346,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Saturazione in vista: %@" + "value" : "Satelliti in vista: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "視野内衛星数: %@" } }, "sr" : { @@ -25869,7 +30392,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Risparmiare" + "value" : "Salva" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" } }, "pl" : { @@ -25912,6 +30441,12 @@ "value" : "Salvare le impostazioni del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル設定を保存" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -25952,6 +30487,12 @@ "value" : "Salva la configurazione per %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ の設定を保存" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -25998,6 +30539,12 @@ "value" : "Salvare la configurazione utente in %@?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー設定を %@ に保存しますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26026,6 +30573,12 @@ "value" : "Salva un CSV con i dettagli del messaggio di test di portata, attualmente disponibile solo sui dispositivi ESP32 con un server web." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レンジテストメッセージの詳細をCSVで保存します。現在、Webサーバーを持つESP32デバイスでのみ利用可能です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26046,6 +30599,22 @@ } } }, + "Scan this QR code to add %@ to another device." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このQRコードをスキャンして、%@ を別のデバイスに追加してください。" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "掃描這個QR code 以便將 %@ 增加到另一個裝置。" + } + } + } + }, "Screen on for" : { "localizations" : { "it" : { @@ -26054,6 +30623,12 @@ "value" : "Schermo acceso per" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面オン時間" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26082,6 +30657,12 @@ "value" : "Ricerca" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検索" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26104,6 +30685,12 @@ "value" : "Secondo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "秒" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26144,6 +30731,12 @@ "value" : "Secondario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "副" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26190,6 +30783,12 @@ "value" : "Chiave amministrativa secondaria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "副管理者キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26224,6 +30823,12 @@ "value" : "Sicurezza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セキュリティ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26258,6 +30863,12 @@ "value" : "Configurazione della sicurezza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セキュリティ設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26292,6 +30903,12 @@ "value" : "Le impostazioni di configurazione della sicurezza richiedono una versione del firmware 2.5+" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "セキュリティ設定にはファームウェアバージョン2.5以上が必要です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26338,6 +30955,12 @@ "value" : "Selezionare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26384,6 +31007,12 @@ "value" : "Selezionare un canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネルを選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26406,6 +31035,12 @@ "value" : "Selezionare una conversazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会話を選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26428,6 +31063,12 @@ "value" : "Selezionare un tipo di conversazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "会話タイプを選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26443,7 +31084,20 @@ } }, "Select a node from the drop down to manage connected or remote devices." : { - + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ドロップダウンからノードを選択して、接続済みまたはリモートデバイスを管理してください。" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "從下拉選單中選擇一個節點,以管理已連接或遠端的裝置。" + } + } + } }, "Select a Trace Route" : { "localizations" : { @@ -26453,6 +31107,12 @@ "value" : "Selezionare un percorso di tracciamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルートを選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26475,6 +31135,12 @@ "value" : "Selezionare il canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル選択" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26515,6 +31181,12 @@ "value" : "Selezionare un nodo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード選択" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26561,6 +31233,12 @@ "value" : "Inviare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26589,6 +31267,12 @@ "value" : "Invia ${messaggioContenuto} a ${canaleNumero}" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${messageContent} をチャンネル ${channelNumber} に送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26611,6 +31295,12 @@ "value" : "Invia ${messaggioContenuto} a ${nodoNumero}" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "${messageContent} をノード ${nodeNumber} に送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26625,28 +31315,6 @@ } } }, - "Send a Direct Message" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inviare un messaggio diretto" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пошаљи директну поруку" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "發送私訊" - } - } - } - }, "Send a Group Message" : { "localizations" : { "de" : { @@ -26661,6 +31329,12 @@ "value" : "Inviare un messaggio di gruppo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループメッセージを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26683,6 +31357,12 @@ "value" : "Invia un heartbeat per pubblicizzare la presenza del server." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバーの存在を通知するためのハートビートを送信します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -26699,6 +31379,12 @@ "value" : "Inviare un messaggio a un certo canale meshtastic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "特定のMeshtasticチャンネルにメッセージを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26713,34 +31399,18 @@ } } }, - "Send a message to a certain meshtastic node" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inviare un messaggio a un certo nodo meshtastico" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пошаљи поруку одређеном мештастик чвору" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "向特定 Meshtastic 節點發送訊息" - } - } - } - }, "Send a position on the primary channel when the user button is triple clicked." : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia una posizione sul canale primario quando si fa triplo clic sul pulsante utente." + "value" : "Invia una posizione sul canale principale quando si fa triplo clic sul pulsante utente." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーボタンが3回クリックされたときにプライマリチャンネルで位置を送信します。" } }, "sr" : { @@ -26777,6 +31447,12 @@ "value" : "Inviare uno spegnimento al nodo a cui si è connessi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続しているノードにシャットダウン信号を送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26805,6 +31481,12 @@ "value" : "Inviare un waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントを送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26824,7 +31506,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare una notifica esterna sul campanello." + "value" : "Invia una campana ASCII con un messaggio di avviso. Utile per attivare notifiche esterne alla ricezione della campana." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラートメッセージ付きASCIIベルを送信。ベルでの外部通知のトリガーに便利です。" } }, "sr" : { @@ -26861,6 +31549,12 @@ "value" : "Invia la campana" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベル送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26907,6 +31601,12 @@ "value" : "Inviare il battito cardiaco" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ハートビート送信" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26947,6 +31647,12 @@ "value" : "Inviare il riavvio OTA" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OTA再起動送信" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -26975,6 +31681,12 @@ "value" : "Intervallo del mittente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信者間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27009,6 +31721,12 @@ "value" : "Sensore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "センサー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27037,6 +31755,12 @@ "value" : "Opzioni del sensore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "センサーオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27059,6 +31783,12 @@ "value" : "Opzioni del sensore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "センサーオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27093,6 +31823,12 @@ "value" : "Inviato un canale per: %@ Canale Indice %d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ のチャンネルを送信しました(チャンネルインデックス %d)" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27151,6 +31887,12 @@ "value" : "Inviato un LoRa.Config per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ にLoRa設定を送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27210,6 +31952,12 @@ "value" : "Inviato un pacchetto di posizione dal GPS del dispositivo Apple al nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "AppleデバイスのGPSからノード %@ に位置パケットを送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27268,6 +32016,12 @@ "value" : "Ha inviato una richiesta di tracciamento della rotta al nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード %@ にトレースルート要求を送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27326,6 +32080,12 @@ "value" : "Inviato un pacchetto Waypoint da: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ からウェイポイントパケットを送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27384,6 +32144,12 @@ "value" : "Inviato messaggio %@ da %@ a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ %@ を %@ から %@ に送信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27416,34 +32182,6 @@ } } }, - "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wird an andere Knoten im Netz gesendet, damit diese einen gemeinsamen geheimen Schlüssel berechnen können." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inviato agli altri nodi della rete per consentire loro di calcolare una chiave segreta condivisa." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Послато другим чворовима на меш мрежи како би им омогућило да израчунају заједнички тајни кључ." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "傳送到網路上的其他節點,以便共同計算一組共享私鑰。" - } - } - } - }, "Sequence number" : { "localizations" : { "de" : { @@ -27458,6 +32196,12 @@ "value" : "Numero di sequenza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シーケンス番号" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27486,6 +32230,12 @@ "value" : "Sequenza: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シーケンス: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27520,6 +32270,12 @@ "value" : "Seriale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27578,6 +32334,12 @@ "value" : "Configurazione seriale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアル設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27624,6 +32386,12 @@ "value" : "Console seriale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアルコンソール" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27652,6 +32420,12 @@ "value" : "Console seriale tramite l'API Stream." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stream API経由のシリアルコンソール。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27692,6 +32466,12 @@ "value" : "Configurazione modulo seriale ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シリアルモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27732,6 +32512,12 @@ "value" : "Serie" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "系列" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27760,6 +32546,12 @@ "value" : "Server" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27788,6 +32580,12 @@ "value" : "Indirizzo del server" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバーアドレス" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27810,6 +32608,12 @@ "value" : "Opzione server" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サーバーオプション" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -27826,6 +32630,12 @@ "value" : "Set" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27866,6 +32676,12 @@ "value" : "Impostare la regione LoRa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa地域を設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27906,6 +32722,12 @@ "value" : "Impostare i pin GPIO per RXD e TXD." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RXDとTXDのGPIOピンを設定します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27920,6 +32742,16 @@ } } }, + "Set to current location" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の位置に設定" + } + } + } + }, "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs." : { "localizations" : { "it" : { @@ -27928,6 +32760,12 @@ "value" : "Imposta il numero massimo di hop, l'impostazione predefinita è 3. L'aumento degli hop aumenta anche la congestione e deve essere usato con attenzione. I messaggi di broadcasting a un hop non riceveranno ACK." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大ホップ数を設定します。デフォルトは3です。ホップ数を増やすと輻輳も増加するため、慎重に使用してください。0ホップのブロードキャストメッセージはACKを受信しません。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -27942,6 +32780,16 @@ } } }, + "Sets the screen clock format to 12-hour." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面の時計表示を12時間形式に設定します。" + } + } + } + }, "Settings" : { "localizations" : { "de" : { @@ -27968,6 +32816,12 @@ "value" : "Impostazioni" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28026,6 +32880,12 @@ "value" : "Settantadue ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "72時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28058,6 +32918,22 @@ } } }, + "Share Contact QR" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "連絡先QRを共有" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "分享聯絡人 QR 碼" + } + } + } + }, "Share QR Code" : { "localizations" : { "de" : { @@ -28084,6 +32960,12 @@ "value" : "Condividi il codice QR" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコードを共有" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28130,6 +33012,12 @@ "value" : "Condividi il codice QR e il link" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QRコードとリンクを共有" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28158,6 +33046,12 @@ "value" : "Chiave condivisa" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "共有キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28198,6 +33092,12 @@ "value" : "Condividere i canali Meshtastic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticチャンネル共有" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28244,6 +33144,12 @@ "value" : "Nome breve" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短い名前" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28264,40 +33170,6 @@ } } }, - "Short Name: %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kurzname: %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome breve: %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Кратко име: %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "短名称: %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "簡短名稱:%@" - } - } - } - }, "Short Range - Fast" : { "localizations" : { "it" : { @@ -28306,6 +33178,12 @@ "value" : "Corto raggio - Veloce" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短距離 - 高速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28328,6 +33206,12 @@ "value" : "Corto raggio - Lento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短距離 - 低速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28350,6 +33234,12 @@ "value" : "Corto raggio - Turbo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "短距離 - ターボ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28363,6 +33253,9 @@ } } } + }, + "Show a confirmation dialog before performing the factory reset" : { + }, "Show alerts" : { "localizations" : { @@ -28378,6 +33271,12 @@ "value" : "Mostra avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラート表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28406,6 +33305,12 @@ "value" : "Mostra avvisi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アラート表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28434,6 +33339,12 @@ "value" : "Mostra i nodi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28462,6 +33373,12 @@ "value" : "Mostra sullo schermo del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面に表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28490,6 +33407,12 @@ "value" : "Mostra sulla mappa della mesh." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッシュマップに表示。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28504,7 +33427,7 @@ } } }, - "Show Waypoints " : { + "Show Waypoints" : { "localizations" : { "de" : { "stringUnit" : { @@ -28518,6 +33441,12 @@ "value" : "Mostra waypoint " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントを表示" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28532,21 +33461,8 @@ } } }, - "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra le informazioni relative alla radio Lora collegata via bluetooth. È possibile scorrere il dito verso sinistra per scollegare la radio e premere a lungo per avviare l'attività live." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "顯示透過藍牙連接的 LoRa 裝置資訊。您可以向左滑動來斷開裝置,長按則可啟動即時活動。" - } - } - } + "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." : { + }, "Shut Down" : { "localizations" : { @@ -28562,6 +33478,12 @@ "value" : "Spegnimento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シャットダウン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28590,6 +33512,12 @@ "value" : "Spegnere il nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードをシャットダウンしますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28618,6 +33546,12 @@ "value" : "Arresto del nodo?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードをシャットダウンしますか?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28652,6 +33586,12 @@ "value" : "Spegnimento in caso di perdita di alimentazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電源喪失時にシャットダウン" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28692,6 +33632,12 @@ "value" : "Segnale %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "信号 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28732,6 +33678,12 @@ "value" : "Semplice" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シンプル" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28772,6 +33724,12 @@ "value" : "Singapore 923MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シンガポール 923MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28812,6 +33770,12 @@ "value" : "Sei ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "6時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -28844,40 +33808,6 @@ } } }, - "skiing" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "skitour" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "tour sciistico" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "ски тура" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "滑雪之旅" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "滑雪" - } - } - } - }, "Skiing" : { "localizations" : { "de" : { @@ -28892,6 +33822,12 @@ "value" : "Sci" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スキー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28920,6 +33856,12 @@ "value" : "Posizione intelligente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマート位置" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28942,6 +33884,12 @@ "value" : "SNR" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28964,6 +33912,12 @@ "value" : "SNR %@ dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@ dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -28986,6 +33940,12 @@ "value" : "SNR %@dB" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SNR %@dB" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29008,6 +33968,12 @@ "value" : "Umidità del suolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "土壌水分" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29024,6 +33990,12 @@ "value" : "Temperatura del suolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "土壌温度" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29040,6 +34012,12 @@ "value" : "Specifica la durata dell'uscita del GPIO monitorato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視対象GPIOの出力時間を指定。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29068,6 +34046,12 @@ "value" : "Velocità" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "速度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29096,6 +34080,12 @@ "value" : "Velocità %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "速度 %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29124,6 +34114,12 @@ "value" : "Velocità: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "速度: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29138,6 +34134,16 @@ } } }, + "Sponsor App Development" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリ開発をスポンサー" + } + } + } + }, "Spread Factor" : { "localizations" : { "it" : { @@ -29146,6 +34152,12 @@ "value" : "Fattore di diffusione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "拡散係数" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29186,6 +34198,12 @@ "value" : "SSID" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSID" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29226,6 +34244,18 @@ "value" : "סטנדרטי" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predefinito" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "標準" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29269,7 +34299,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Standard Silenzioso" + "value" : "Predefinito Silenzioso" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "標準ミュート" } }, "pl" : { @@ -29330,6 +34366,12 @@ "value" : "Inizio" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29370,6 +34412,12 @@ "value" : "Stato Intervallo di trasmissione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "状態ブロードキャスト間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29389,7 +34437,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Memorizzare e inoltrare" + "value" : "Salva & Inoltra" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送" } }, "sr" : { @@ -29417,7 +34471,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "Configurazione Store & Forward" + "value" : "Configurazione Salva & Inoltra" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送設定" } }, "sr" : { @@ -29460,6 +34520,12 @@ "value" : "Configurazione del modulo Store & Forward ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送モジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29500,6 +34566,12 @@ "value" : "I server Store and Forward richiedono un dispositivo ESP32 con PSRAM o Linux Native." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓄積転送サーバーには、PSRAM搭載のESP32デバイスまたはLinux Nativeが必要です。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -29522,6 +34594,12 @@ "value" : "Abbonati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "購読済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29544,11 +34622,23 @@ "value" : "Sottosistema" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サブシステム" + } + }, "sr" : { "stringUnit" : { "state" : "translated", "value" : "Подсистем" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "子系統" + } } } }, @@ -29566,6 +34656,12 @@ "value" : "Supportato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポート済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29588,6 +34684,12 @@ "value" : "I sensori I2C supportati vengono rilevati automaticamente: BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 e SHTC3." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポートされているI2C接続センサーは自動的に検出されます。センサーはBMP280、BME280、BME680、MCP9808、INA219、INA260、LPS22、およびSHTC3です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29616,6 +34718,12 @@ "value" : "Tabella" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テーブル" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29632,6 +34740,12 @@ }, "Taiwan" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "台湾" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29660,6 +34774,12 @@ "value" : "TAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAK" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29694,6 +34814,12 @@ "value" : "Tracker TAK" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAKトラッカー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29722,6 +34848,12 @@ "value" : "Prende l'URL di un canale Meshtastic e salva le impostazioni del canale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeshtasticチャンネルURLを取得し、チャンネル設定を保存します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -29736,6 +34868,22 @@ } } }, + "Takes a Meshtastic contact URL and saves it to the nodes database" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic連絡先URLを取得し、ノードデータベースに保存します" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "將 Meshtastic 聯絡人的網址儲存到節點資料庫中。" + } + } + } + }, "Tapback" : { "localizations" : { "de" : { @@ -29762,6 +34910,12 @@ "value" : "Risposta di Tapback" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップバック" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29820,6 +34974,12 @@ "value" : "Telemetria (sensori)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29878,6 +35038,12 @@ "value" : "Configurazione della telemetria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリ設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29936,6 +35102,12 @@ "value" : "Configurazione del modulo di telemetria ricevuta: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テレメトリーモジュール設定を受信しました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -29994,6 +35166,12 @@ "value" : "Telemetria ricevuta per: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ のテレメトリーを受信しました" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30040,6 +35218,12 @@ "value" : "Temp" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "温度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30068,6 +35252,12 @@ "value" : "Temperatura" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "温度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30108,6 +35298,12 @@ "value" : "Dieci minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "10分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30166,6 +35362,12 @@ "value" : "Dieci secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "10秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30212,6 +35414,12 @@ "value" : "Chiave amministrativa terziaria" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "第三管理者キー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30258,6 +35466,12 @@ "value" : "Messaggio di testo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキストメッセージ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30298,6 +35512,12 @@ "value" : "Display TFT a colori" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TFTフルカラーディスプレイ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30320,6 +35540,12 @@ "value" : "Thailandia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30342,6 +35568,12 @@ "value" : "Il tempo di attesa prima che il pacchetto venga considerato completato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パケットが完了したと見なすまでの待機時間。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30364,6 +35596,12 @@ "value" : "La direzione della bussola sullo schermo all'esterno del cerchio punterà sempre verso nord." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画面上の円の外側にあるコンパスの方位は常に北を指します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30392,6 +35630,12 @@ "value" : "Il punto di rugiada è %@ in questo momento." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の露点は %@ です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30414,6 +35658,12 @@ "value" : "La velocità con cui verranno inviati gli aggiornamenti della posizione se la distanza minima è stata soddisfatta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最小距離条件が満たされた場合の位置更新送信の最短間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30436,6 +35686,12 @@ "value" : "Il formato utilizzato per visualizzare le coordinate GPS sullo schermo del dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面でGPS座標を表示する形式。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30464,6 +35720,12 @@ "value" : "Gli ultimi 4 dell'indirizzo MAC del dispositivo vengono aggiunti al nome breve per impostare il nome BLE del dispositivo. Il nome breve può avere una lunghezza massima di 4 byte." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイスのBLE名を設定するため、MACアドレスの末尾4桁が短縮名に追加されます。短縮名は最大4バイトまでです。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30492,6 +35754,12 @@ "value" : "L'intervallo massimo che può trascorrere senza che un nodo trasmetta una posizione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードが位置をブロードキャストしない最大間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30514,6 +35782,12 @@ "value" : "Le applicazioni Meshtastic Apple supportano la versione firmware %@ e successive." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtastic Appleアプリはファームウェアバージョン %@ 以上をサポートしています。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30542,6 +35816,12 @@ "value" : "La variazione di distanza minima in metri da considerare per la trasmissione di una posizione intelligente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマート位置ブロードキャストで考慮される最小距離変化(メートル)。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30570,6 +35850,12 @@ "value" : "La chiave pubblica più recente di questo nodo non corrisponde alla chiave registrata in precedenza. È possibile eliminare il nodo e fargli scambiare nuovamente le chiavi, ma questo potrebbe indicare un problema di sicurezza più serio. Contattare l'utente attraverso un altro canale fidato per determinare se la modifica della chiave è dovuta a un reset di fabbrica o a un'altra azione intenzionale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードの最新の公開キーが以前記録されたキーと一致しません。ノードを削除して再度キー交換を行うことができますが、これはより深刻なセキュリティ問題を示している可能性もあります。信頼できる別のチャンネルを通じてユーザーに連絡し、キーの変更が工場リセットやその他の intentional action によるものかどうかを確認してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30616,6 +35902,12 @@ "value" : "Il pacchetto è troppo grande" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パケットが大きすぎます" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30662,6 +35954,12 @@ "value" : "La chiave pubblica primaria autorizzata a inviare messaggi di amministrazione a questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードに管理メッセージを送信する権限を持つプライマリ公開キー。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30690,6 +35988,12 @@ "value" : "La chiave pubblica non corrisponde alla chiave registrata. È possibile eliminare il nodo e fargli scambiare nuovamente le chiavi, ma questo potrebbe indicare un problema di sicurezza più serio. Contattare l'utente attraverso un altro canale fidato, per determinare se la modifica della chiave è dovuta a un reset di fabbrica o a un'altra azione intenzionale." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "公開キーが記録されたキーと一致しません。ノードを削除して再度キー交換を行うことができますが、これはより深刻なセキュリティ問題を示している可能性があります。信頼できる別のチャンネルを通じてユーザーに連絡し、キーの変更が工場リセットやその他の意図的な操作によるものかどうか確認してください。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30718,6 +36022,12 @@ "value" : "La regione in cui si utilizzeranno le radio." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "無線機を使用する地域。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30746,6 +36056,12 @@ "value" : "L'argomento principale da usare per MQTT." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTに使用するルートトピック。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30774,6 +36090,12 @@ "value" : "I ruoli di router sono progettati per posizioni elevate, come le cime delle montagne e le torri. Questo nodo deve essere in grado di avere una buona connessione diretta con la maggior parte dei nodi della rete, altrimenti danneggia significativamente la rete." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルーター役割は山頂や塔のような見晴らしの良い高所での使用を想定して設計されています。このノードは、ネットワーク内の大部分のノードと良好な直接接続を保持できる必要があります。そうでなければ、ネットワークに深刻な影響を与えることになります。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -30796,6 +36118,12 @@ "value" : "La chiave pubblica secondaria autorizzata a inviare messaggi di amministrazione a questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードに管理メッセージを送信する権限を持つセカンダリ公開キー。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30824,6 +36152,12 @@ "value" : "Il dispositivo specificato si è disconnesso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "指定されたデバイスが切断されました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30846,6 +36180,12 @@ "value" : "Lo stato del LED (acceso/spento)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LEDの状態(オン/オフ)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30874,6 +36214,12 @@ "value" : "La chiave pubblica terziaria autorizzata a inviare messaggi di amministrazione a questo nodo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードに管理メッセージを送信する権限を持つ三次公開キー。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30902,6 +36248,12 @@ "value" : "L'URL per le impostazioni del canale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル設定のURL" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30916,27 +36268,24 @@ } } }, - "There has been no response to a request for device metadata over the admin channel for this node." : { + "The URL for the node to add" : { "localizations" : { - "it" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Non è stata data risposta a una richiesta di metadati del dispositivo sul canale di amministrazione per questo nodo." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Није било одговора на захтев за метаподатке уређаја преко административног канала за овај чвор." + "value" : "追加するノードのURL" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "此節點的管理頻道未回應設備中繼資料的請求。" + "value" : "要新增的節點網址" } } } + }, + "There has been no response to a request for device metadata via PKC admin for this node." : { + }, "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" : { @@ -30946,6 +36295,12 @@ "value" : "Queste impostazioni saranno %@ canali. La configurazione LoRa corrente verrà sostituita; se vengono apportate modifiche sostanziali alla configurazione LoRa, il dispositivo si riavvierà" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これらの設定はチャンネルを%@します。現在のLoRa設定は置き換えられ、LoRa設定に大幅な変更があった場合、デバイスは再起動します" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -30986,6 +36341,12 @@ "value" : "Trenta minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "30分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31044,6 +36405,12 @@ "value" : "Trenta secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "30秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31102,6 +36469,12 @@ "value" : "Trentasei ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "36時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31142,6 +36515,12 @@ "value" : "Questa conversazione sarà cancellata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この会話は削除されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31164,6 +36543,12 @@ "value" : "La risposta potrebbe richiedere un po' di tempo e verrà visualizzata nel registro delle rotte di tracciamento per il nodo a cui è stata inviata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これには時間がかかる場合があります。応答は送信先ノードのトレースルートログに表示されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31178,28 +36563,6 @@ } } }, - "This could take a while. The response will appear in the trace route log for the node it was sent to." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "L'operazione potrebbe richiedere un po' di tempo. La risposta apparirà nel registro delle rotte di tracciamento per il nodo a cui è stata inviata." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ово може потрајати. Одговор ће се појавити у евиденцији трасе праћења за чвор којем је послат." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "這可能需要一段時間。回應將會顯示在被發送節點的路由追蹤紀錄中。" - } - } - } - }, "This device will send out range test messages on the selected interval." : { "localizations" : { "it" : { @@ -31208,6 +36571,12 @@ "value" : "Il dispositivo invia messaggi di test di portata all'intervallo selezionato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスは選択した間隔でレンジテストメッセージを送信します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31242,6 +36611,12 @@ "value" : "È probabile che questo messaggio non sia stato consegnato." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージは配信されなかった可能性があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31264,6 +36639,12 @@ "value" : "Questo nodo non supporta alcun modulo configurabile." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このノードは設定可能なモジュールをサポートしていません。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31280,6 +36661,12 @@ "value" : "In questo modo si disattiva la posizione fissa e si rimuove la posizione attualmente impostata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これにより固定位置が無効になり、現在設定されている位置が削除されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31302,6 +36689,12 @@ "value" : "In questo modo si invia la posizione corrente dal telefono e si abilita la posizione fissa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これにより、お使いの携帯電話から現在位置を送信し、固定位置を有効にします。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31348,6 +36741,12 @@ "value" : "Tre ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "3時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31406,6 +36805,12 @@ "value" : "Tre secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "3秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31464,6 +36869,12 @@ "value" : "Pollici in giù" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "👎" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31522,6 +36933,12 @@ "value" : "Pollici in su" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "👍" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31568,6 +36985,12 @@ "value" : "Tempo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時刻" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31596,6 +37019,12 @@ "value" : "Timbro del tempo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムスタンプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31624,6 +37053,12 @@ "value" : "Fuso orario" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムゾーン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31652,6 +37087,12 @@ "value" : "Fuso orario per le date sullo schermo del dispositivo e sul registro." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面とログの日付用タイムゾーン。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31692,6 +37133,12 @@ "value" : "Timeout" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムアウト" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31750,6 +37197,12 @@ "value" : "Timestamp" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイムスタンプ" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -31790,6 +37243,12 @@ "value" : "Tempi e formati" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タイミング・フォーマット" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31812,6 +37271,12 @@ "value" : "TLS abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "TLS有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31834,6 +37299,12 @@ }, "To comply with privacy laws like CCPA and GDPR, we avoid sharing exact location data. Instead, we use anonymized or approximate (imprecise) location information to protect your privacy." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CCPAやGDPRなどのプライバシー法に準拠するため、正確な位置データの共有は避けています。代わりに、あなたのプライバシーを保護するために匿名化または近似(不正確)の位置情報を使用します。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -31850,6 +37321,12 @@ "value" : "Argomento: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トピック: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31878,6 +37355,12 @@ "value" : "Totale" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "合計" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31900,6 +37383,12 @@ "value" : "Totale PAX" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "総PAX" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -31928,6 +37417,12 @@ "value" : "Percorso di tracciamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31942,6 +37437,22 @@ } } }, + "Trace Route (in %@s)" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート(%@秒)" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "追蹤路由(在 %@ 秒)" + } + } + } + }, "Trace Route Log" : { "localizations" : { "it" : { @@ -31950,6 +37461,12 @@ "value" : "Registro del percorso di tracciamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルートログ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -31990,6 +37507,12 @@ "value" : "Traccia Richiesta di rotta restituita: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート要求が返されました: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32030,6 +37553,12 @@ "value" : "Traccia del percorso inviato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルート送信済み" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32052,6 +37581,12 @@ "value" : "Traccia del percorso inviato a %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ にトレースルートを送信しました" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32074,6 +37609,12 @@ "value" : "La rotta di tracciamento verso %@ non è stata inviata." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ へのトレースルートは送信されませんでした。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32096,6 +37637,12 @@ "value" : "La rotta di traccia era limitata dalla velocità. È possibile inviare una rotta di tracciamento al massimo una volta ogni trenta secondi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレースルートの送信レートが制限されました。トレースルートは30秒ごとに最大1回送信できます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32112,6 +37659,12 @@ }, "Tracker" : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トラッカー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32123,6 +37676,12 @@ "state" : "translated", "value" : "追踪器" } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "追蹤器" + } } } }, @@ -32140,6 +37699,12 @@ "value" : "Traffico" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トラフィック" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32162,6 +37727,12 @@ "value" : "Dati di trasmissione (txd) Pin GPIO" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信データ(TXD)GPIOピン" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32184,6 +37755,12 @@ "value" : "Trasmissione abilitata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "送信有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32212,6 +37789,12 @@ "value" : "Tratta il doppio tocco sugli accelerometri supportati come una pressione di un tasto utente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポートされている加速度計でのダブルタップをユーザーボタン押下として扱います。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32240,6 +37823,12 @@ "value" : "Tipo di innesco" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トリガータイプ" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32262,6 +37851,12 @@ "value" : "Ping ad hoc a triplo clic" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トリプルクリック アドホックPing" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32290,6 +37885,12 @@ "value" : "Riprova" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再試行" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32330,6 +37931,12 @@ "value" : "Dodici ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "12時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32388,6 +37995,12 @@ "value" : "Ventiquattro ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "24時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32446,6 +38059,12 @@ "value" : "Due ore" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32504,6 +38123,12 @@ "value" : "Due minuti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2分" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32562,6 +38187,12 @@ "value" : "Due secondi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "2秒" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32602,6 +38233,12 @@ "value" : "Trasmissione UDP" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDPブロードキャスト" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -32618,6 +38255,12 @@ "value" : "Ucraina 433MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウクライナ 433MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32640,6 +38283,12 @@ "value" : "Ucraina 868MHz" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウクライナ 868MHz" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32662,6 +38311,12 @@ "value" : "Non preferito" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入りを解除" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32684,6 +38339,12 @@ "value" : "Non sano" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不健康" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32706,6 +38367,12 @@ "value" : "Insalubre per i gruppi sensibili" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "敏感なグループには不健康" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32728,6 +38395,12 @@ "value" : "Stati Uniti" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アメリカ合衆国" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32750,6 +38423,12 @@ "value" : "Unità visualizzate sullo schermo del dispositivo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "デバイス画面に表示される単位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -32790,6 +38469,12 @@ "value" : "Mercatore Universale Trasverso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユニバーサル横メルカトル図法" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32822,6 +38507,34 @@ } } }, + "unknown" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "sconosciuto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, + "sr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "непознато" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "未知" + } + } + } + }, "Unknown" : { "localizations" : { "fr" : { @@ -32842,6 +38555,12 @@ "value" : "Sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32900,6 +38619,12 @@ "value" : "Età sconosciuta" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明な経過時間" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32932,6 +38657,26 @@ } } }, + "Unmessagable" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージ不可" + } + } + } + }, + "Unmonitored" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視なし" + } + } + } + }, "Unset" : { "localizations" : { "de" : { @@ -32958,6 +38703,12 @@ "value" : "Non impostato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未設定" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -32998,6 +38749,12 @@ "value" : "Non supportato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポート対象外" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33038,6 +38795,12 @@ "value" : "Rilevata versione firmware non supportata, impossibile connettersi al dispositivo." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サポートされていないファームウェアバージョンが検出されました。デバイスに接続できません。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33096,6 +38859,12 @@ "value" : "Su" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33136,6 +38905,12 @@ "value" : "Su Giù 1" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "上下 1" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33182,6 +38957,12 @@ "value" : "fino a %@ di distanza" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大 %@ 離れています" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33222,6 +39003,12 @@ "value" : "Intervallo di aggiornamento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新間隔" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33262,6 +39049,12 @@ "value" : "Aggiornare il firmware" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアを更新" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33302,6 +39095,12 @@ "value" : "Dati aggiornati sulle statistiche dei nodi." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノード統計データを更新しました。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33330,6 +39129,12 @@ "value" : "Aggiornato: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新日時: %@" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33352,6 +39157,12 @@ "value" : "Uplink abilitato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アップリンク有効" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33380,6 +39191,12 @@ "value" : "Tempo di attività" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "稼働時間" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -33399,13 +39216,22 @@ } } } + }, + "Usage and Crash Data" : { + }, "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" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del buzzer configurata dal dispositivo." + "value" : "Utilizzare un'uscita PWM (come il cicalino RAK) per le sintonie invece di un'uscita on/off. In questo modo si ignorano le impostazioni di uscita, durata e attivazione e si utilizza invece l'opzione GPIO del cicalino configurata dal dispositivo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オン/オフ出力ではなく、PWM出力(RAKブザーなど)をチューンに使用してください。これにより、出力、出力時間、アクティブ設定は無視され、代わりにデバイス設定のブザーGPIOオプションが使用されます。" } }, "sr" : { @@ -33436,6 +39262,12 @@ "value" : "Utilizzare I2S come cicalino" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "I2Sをブザーとして使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33450,6 +39282,16 @@ } } }, + "Use my Location" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分の位置を使用" + } + } + } + }, "Use Preset" : { "localizations" : { "it" : { @@ -33458,6 +39300,12 @@ "value" : "Utilizzare la preimpostazione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プリセットを使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33486,6 +39334,12 @@ "value" : "Utilizzare il cicalino PWM" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PWMブザーを使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33514,6 +39368,12 @@ "value" : "Si usa per creare una chiave condivisa con un dispositivo remoto." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リモート デバイスとの共有キーを作成するために使用されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33528,6 +39388,16 @@ } } }, + "Used to identify unmonitored or infrastructure nodes so that messaging is not avaliable to nodes that will never respond." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "監視されていないまたはインフラストラクチャノードを識別するために使用され、応答しないノードにはメッセージング機能が利用できないようにします。" + } + } + } + }, "User" : { "localizations" : { "de" : { @@ -33554,6 +39424,12 @@ "value" : "Utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33594,6 +39470,12 @@ "value" : "Configurazione utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー設定" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33622,6 +39504,12 @@ "value" : "Dettagli utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー詳細" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33650,6 +39538,12 @@ "value" : "Id utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーID" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33678,6 +39572,12 @@ "value" : "Disconnessione avviata dall'utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーによる切断" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33724,6 +39624,12 @@ "value" : "Nome utente" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザー名" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33764,6 +39670,12 @@ "value" : "Utilizza una resistenza di pullup" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プルアップ抵抗を使用" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33786,6 +39698,12 @@ "value" : "Utilizza la connessione di rete del telefono per connettersi a MQTT." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スマートフォンのネットワーク接続を利用してMQTTに接続します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33820,6 +39738,12 @@ "value" : "Direzione del veicolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "車両方位" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33848,6 +39772,12 @@ "value" : "Velocità del veicolo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "車両速度" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33876,6 +39806,12 @@ "value" : "La versione %1$@ include sostanziali ottimizzazioni di rete e modifiche estese ai dispositivi e alle applicazioni client. Sono supportati solo i nodi versione %2$@ e superiori." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン%1$@には、ネットワークの大幅な最適化と、デバイスおよびクライアントアプリへの広範な変更が含まれています。サポートされるノードはバージョン%2$@以降のみです。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33903,6 +39839,18 @@ "state" : "new", "value" : "Version: %1$@ (%2$@)" } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン: %@ (%@)" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本: %1$@ (%2$@)" + } } } }, @@ -33920,6 +39868,12 @@ "value" : "Versione: %1$@ (%2$@) " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "バージョン: %1$@ (%2$@)" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33948,6 +39902,12 @@ "value" : "Molto malsano" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "非常に不健康" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -33976,6 +39936,12 @@ "value" : "Via Lora" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoRa経由" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34004,6 +39970,12 @@ "value" : "Via Mqtt" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTT経由" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34050,6 +40022,12 @@ "value" : "Tensione" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電圧" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34090,6 +40068,12 @@ "value" : "Volt %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volts %@" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34124,6 +40108,12 @@ "value" : "In attesa. . ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "待機中" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34164,6 +40154,12 @@ "value" : "In attesa di essere riconosciuti. . ." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "承認待ち. . ." + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34186,6 +40182,12 @@ "value" : "Svegliare lo schermo al tocco o al movimento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップまたはモーションで画面を起動" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34200,40 +40202,6 @@ } } }, - "walk" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "gehen" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "passeggiata" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "шетња" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "步行" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "走路" - } - } - } - }, "Walking" : { "localizations" : { "de" : { @@ -34248,6 +40216,12 @@ "value" : "Camminare" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "歩行" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34282,6 +40256,12 @@ "value" : "Onda" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "波" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34302,6 +40282,16 @@ } } }, + "Waypoint Failed to Send" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントの送信に失敗" + } + } + } + }, "Waypoint Options" : { "localizations" : { "de" : { @@ -34316,6 +40306,12 @@ "value" : "Opzioni Waypoint" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェイポイントオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34356,6 +40352,12 @@ "value" : "Pacchetto Waypoint ricevuto dal nodo: %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードから受信したウェイポイント パケット: %@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34402,6 +40404,12 @@ "value" : "Condizioni meteo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "気象条件" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34430,6 +40438,12 @@ "value" : "Lampeggiatore web" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェブフラッシャー" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34452,6 +40466,12 @@ "value" : "Sito web" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウェブサイト" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34480,6 +40500,12 @@ "value" : "Peso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "重量" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34505,6 +40531,12 @@ "value" : "Che cosa significa il lucchetto?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "鍵マークの意味は?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34539,6 +40571,12 @@ "value" : "Che cos'è Meshtastic?" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meshtasticとは?" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34567,6 +40605,12 @@ "value" : "Cosa fa la modalità operatore con licenza:\n* Imposta il nome del nodo con il proprio nominativo\n* Trasmette informazioni sul nodo ogni 10 minuti\n* Sovrascrive la frequenza, il dutycycle e la potenza di trasmissione\n* Disabilita la crittografia" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ライセンス操作者モードの機能:\n* ノード名をコールサインに設定\n* 10分ごとにノード情報をブロードキャスト\n* 周波数、デューティサイクル、送信電力をオーバーライド\n* 暗号化を無効化" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34601,6 +40645,12 @@ "value" : "Quando è abilitato, il modulo PAX Counter conta il numero di persone che passano utilizzando il WiFi e il Bluetooth. Per il funzionamento del contatore PAX, sia il WiFI che il Bluetooth devono essere disattivati." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PAXカウンターモジュールを有効にすると、WiFiとBluetoothを使用して通過する人数をカウントします。PAXカウンターを動作させるには、WiFiとBluetoothの両方を無効にする必要があります。" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34635,6 +40685,12 @@ "value" : "Quando si utilizza la modalità GPIO, mantenere l'uscita attiva per questo tempo. " } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIOモードで使用する際、この期間出力をオンに保ちます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34663,6 +40719,12 @@ "value" : "Utilizza o meno la modalità INPUT_PULLUP per il pin GPIO. Si applica solo se la scheda utilizza resistenze di pull-up sul pin" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GPIOピンでINPUT_PULLUPモードを使用するかどうか。ボードがピンでプルアップ抵抗を使用している場合のみ適用されます" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34685,6 +40747,12 @@ "value" : "WiFi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFi" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34719,6 +40787,12 @@ "value" : "Opzioni WiFi" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFiオプション" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34747,6 +40821,12 @@ "value" : "Sospenderà tutto il più possibile, per il ruolo di tracker e sensore questo includerà anche la radio lora. Non utilizzare questa impostazione se si desidera utilizzare il dispositivo con le applicazioni del telefono o se si utilizza un dispositivo senza pulsante utente." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トラッカーとセンサーの役割では、lora無線も含め、すべてのデバイスを可能な限りスリープ状態にします。デバイスをスマートフォンアプリと連携させたい場合、またはユーザーボタンのないデバイスを使用している場合は、この設定を使用しないでください。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34787,6 +40867,12 @@ "value" : "Vento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "風" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -34809,6 +40895,12 @@ "value" : "Direzione del vento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "風向" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34837,6 +40929,12 @@ "value" : "Velocità del vento" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "風速" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34865,6 +40963,12 @@ "value" : "Entro il %@" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 以内" + } + }, "se" : { "stringUnit" : { "state" : "translated", @@ -34893,6 +40997,12 @@ "value" : "x" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "x" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34921,6 +41031,12 @@ "value" : "X: %1$@, Y: %2$d" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$d" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34956,6 +41072,12 @@ "value" : "X: %1$@, Y: %2$f" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$f" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -34991,6 +41113,12 @@ "value" : "X: %1$@, Y: %2$lld" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "X: %1$@, Y: %2$lld" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35020,6 +41148,12 @@ "value" : "y" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "y" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35054,6 +41188,12 @@ "value" : "Ieri" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昨日" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35076,6 +41216,12 @@ "value" : "È anche possibile aggiornare il dispositivo Meshtastic tramite bluetooth utilizzando l'applicazione Nordic DFU." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nordic DFUアプリを使用してBluetoothでMeshtasticデバイスを更新することもできます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35122,6 +41268,12 @@ "value" : "È possibile inviare e ricevere messaggi di canale (chat di gruppo) e messaggi diretti. Da qualsiasi messaggio è possibile premere a lungo per visualizzare le azioni disponibili, come copia, risposta, tapback e cancellazione, nonché i dettagli di consegna." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "チャンネル(グループチャット)とダイレクトメッセージの送受信ができます。任意のメッセージを長押しすると、コピー、返信、タップバック、削除などの利用可能なアクションと配信詳細を表示できます。" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -35162,6 +41314,12 @@ "value" : "La posizione attuale viene impostata come posizione fissa e trasmessa sulla mesh nell'intervallo di posizione." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の位置が固定位置として設定され、位置間隔でメッシュネットワーク上にブロードキャストされます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35196,6 +41354,12 @@ "value" : "Il firmware è aggiornato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファームウェアは最新です" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35224,6 +41388,12 @@ "value" : "Il server MQTT deve supportare TLS." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQTTサーバーはTLSをサポートする必要があります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35240,6 +41410,12 @@ }, "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." : { "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードは設定されたMQTTサーバーに定期的に暗号化されていないマップレポートパケットを送信します。これにはID、短縮名と長い名前、おおよその位置、ハードウェアモデル、役割、ファームウェアバージョン、LoRa地域、モデムプリセット、プライマリチャンネル名が含まれます。" + } + }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", @@ -35253,7 +41429,13 @@ "it" : { "stringUnit" : { "state" : "translated", - "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale primario." + "value" : "La frequenza operativa del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Se il campo è 0, lo slot viene calcolato automaticamente in base al nome del canale principale." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ノードの動作周波数は、地域、モデムプリセット、およびこのフィールドに基づいて計算されます。0の場合、スロットはプライマリチャンネル名に基づいて自動的に計算されます。" } }, "sr" : { @@ -35278,6 +41460,12 @@ "value" : "La vostra posizione è stata inviata con una richiesta di risposta con la loro posizione. Riceverete una notifica quando la posizione verrà restituita." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "位置情報が位置の返信要求と共に送信されました。位置が返信されると通知を受け取ります。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35306,6 +41494,12 @@ "value" : "La vostra regione ha un ciclo di lavoro di %lld%%. MQTT è sconsigliato quando il ciclo di lavoro è limitato, perché il traffico extra sovraccaricherà rapidamente la rete LoRa." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お住まいの地域はデューティサイクルが%lld%%です。デューティサイクル制限がある場合、MQTTの使用は推奨されません。追加のトラフィックによってLoRaメッシュがすぐに圧迫されます。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35334,6 +41528,12 @@ "value" : "La regione ha un ciclo di funzionamento orario del %lld%%; la radio smette di inviare pacchetti quando raggiunge il limite orario." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お住まいの地域は時間あたり%lld%%のデューティサイクル制限があります。無線機が時間制限に達すると、パケットの送信を停止します。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", @@ -35356,6 +41556,12 @@ "value" : "Il file di percorso deve avere entrambe le colonne Latitudine e Longitudine e le intestazioni." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ルートファイルには緯度と経度の列とヘッダーの両方が必要です。" + } + }, "sr" : { "stringUnit" : { "state" : "translated", diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index a49af10d..52ffbb86 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAA2E172F41003D191E /* DatadogCore */; }; + 102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAC2E172F41003D191E /* DatadogCrashReporting */; }; + 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAE2E172F41003D191E /* DatadogLogs */; }; + 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 */; }; 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 */; }; @@ -28,6 +34,7 @@ 2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */; }; 2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */; }; 2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */; }; + 237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */; }; 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; }; 251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; }; 2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; }; @@ -55,12 +62,14 @@ 8D3F8A412D44C2A6009EAAA4 /* PowerMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; - BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; }; + BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; }; BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; + BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */; }; + BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; }; BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; }; @@ -92,6 +101,11 @@ DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; }; + DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; }; + DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; + DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; }; + DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */; }; + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -274,6 +288,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; + 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = ""; }; 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = ""; }; 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultColumns.swift; sourceTree = ""; }; @@ -295,6 +311,7 @@ 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsChartSeries.swift; sourceTree = ""; }; 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsSeriesList.swift; sourceTree = ""; }; 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.swift; sourceTree = ""; }; + 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitedButton.swift; sourceTree = ""; }; 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = ""; }; 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = ""; }; 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = ""; }; @@ -317,13 +334,15 @@ 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; + BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = ""; }; BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactURLHandler.swift; sourceTree = ""; }; + BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = ""; }; BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; @@ -343,6 +362,7 @@ DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; + DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 52.xcdatamodel"; sourceTree = ""; }; DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = ""; }; DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorEnums.swift; sourceTree = ""; }; DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = ""; }; @@ -361,6 +381,12 @@ DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; + DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; + DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; + DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; + DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; + DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistantTips.swift; sourceTree = ""; }; + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelLock.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -410,6 +436,7 @@ DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = ""; }; DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; + DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 51.xcdatamodel"; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; DD6D5A322CA1178300ED3032 /* TraceRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRoute.swift; sourceTree = ""; }; DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 45.xcdatamodel"; sourceTree = ""; }; @@ -571,7 +598,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */, 25A978BA2C13F8ED0003AAE7 /* MeshtasticProtobufs in Frameworks */, + 102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */, + 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */, + 102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */, DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -672,6 +703,7 @@ BCB6137F2C6728E700485544 /* AppIntents */ = { isa = PBXGroup; children = ( + BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */, BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */, BCB613802C67290800485544 /* SendWaypointIntent.swift */, BCB613822C672A2600485544 /* MessageChannelIntent.swift */, @@ -682,7 +714,7 @@ BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */, BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */, - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */, + BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */, ); path = AppIntents; sourceTree = ""; @@ -701,6 +733,7 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */ = { isa = PBXGroup; children = ( + 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */, 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */, DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */, @@ -754,6 +787,7 @@ children = ( DDD5BB0E2C285F92007E03CA /* Logs */, DD93800C2BA74CE3008BEC06 /* Channels */, + DD61937A2863876A00E59241 /* Config */, DD97E96728EFE9A00056DDA4 /* About.swift */, DDD5BB152C28B1E4007E03CA /* AppData.swift */, DDD5BB082C285DDC007E03CA /* AppLog.swift */, @@ -767,7 +801,6 @@ DD3501882852FC3B000FC853 /* Settings.swift */, DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */, DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */, - DD61937A2863876A00E59241 /* Config */, DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */, ); path = Settings; @@ -788,6 +821,7 @@ DD61937A2863876A00E59241 /* Config */ = { isa = PBXGroup; children = ( + DD61937B2863877A00E59241 /* Module */, D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */, D93069072B81DF040066FBC8 /* SaveConfigButton.swift */, DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */, @@ -798,7 +832,6 @@ DD2553582855B52700E55709 /* PositionConfig.swift */, D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */, DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */, - DD61937B2863877A00E59241 /* Module */, ); path = Config; sourceTree = ""; @@ -832,6 +865,7 @@ DD6F65772C6EAB860053C113 /* Help */ = { isa = PBXGroup; children = ( + DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */, DD6F65752C6EA5490053C113 /* AckErrors.swift */, DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */, DD6F657A2C6EC2900053C113 /* LockLegend.swift */, @@ -850,6 +884,7 @@ DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( + DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */, DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */, DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */, DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */, @@ -870,6 +905,7 @@ DD8ED9C6289CE4A100B3B0AB /* Enums */ = { isa = PBXGroup; children = ( + DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */, DDA951592BC6624100CEA535 /* TelemetryWeather.swift */, DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */, DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, @@ -1041,6 +1077,7 @@ DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */, @@ -1052,6 +1089,7 @@ DD5E523D298F5A7D00D21B61 /* Weather */, DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */, + 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */, ); path = Helpers; sourceTree = ""; @@ -1059,8 +1097,10 @@ DDC2E1A526CEB32B0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */, DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, + DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, @@ -1109,6 +1149,7 @@ DDDB26402AABEF7B003AFCB7 /* Helpers */ = { isa = PBXGroup; children = ( + 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */, 231B3F232D087C020069A07D /* Metrics Columns */, DDAD49EB2AFAE82500B4425D /* Map */, DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, @@ -1214,6 +1255,10 @@ packageProductDependencies = ( DD0D3D212A55CEB10066DB71 /* CocoaMQTT */, 25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */, + 102B5EAA2E172F41003D191E /* DatadogCore */, + 102B5EAC2E172F41003D191E /* DatadogCrashReporting */, + 102B5EAE2E172F41003D191E /* DatadogLogs */, + 102B5EB02E172F41003D191E /* DatadogRUM */, ); productName = MeshtasticClient; productReference = DDC2E15426CE248E0042C5E4 /* Meshtastic.app */; @@ -1277,12 +1322,14 @@ se, sr, it, + ja, ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */, 25A978B82C13F8ED0003AAE7 /* XCLocalSwiftPackageReference "MeshtasticProtobufs" */, 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */, + 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */, ); productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */; projectDirPath = ""; @@ -1365,13 +1412,13 @@ 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */, BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */, + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */, 259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */, 259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */, DDDB444829F8A9C900EE2349 /* String.swift in Sources */, DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */, DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */, DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */, - BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */, @@ -1404,6 +1451,7 @@ DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */, 233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */, DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, + 108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */, DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, @@ -1414,15 +1462,19 @@ D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, + DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, 231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */, 25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */, 2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */, DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */, + DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */, DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */, + 237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */, @@ -1434,6 +1486,7 @@ 233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, + DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, @@ -1462,6 +1515,7 @@ DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */, DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */, DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */, + BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */, DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */, DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */, DDDB444629F8A96500EE2349 /* Character.swift in Sources */, @@ -1494,6 +1548,7 @@ DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */, DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, + 108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */, 233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */, DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */, DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, @@ -1551,6 +1606,7 @@ 2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */, BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, + BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, @@ -1565,6 +1621,7 @@ 233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */, BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */, DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */, + DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, @@ -1792,12 +1849,12 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1825,12 +1882,12 @@ INFOPLIST_FILE = Meshtastic/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Meshtastic; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1855,13 +1912,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1887,13 +1944,13 @@ INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Widgets; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.2; + MARKETING_VERSION = 2.6.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1954,6 +2011,14 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */ + 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/DataDog/dd-sdk-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.29.0; + }; + }; 259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -1973,6 +2038,26 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 102B5EAA2E172F41003D191E /* DatadogCore */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCore; + }; + 102B5EAC2E172F41003D191E /* DatadogCrashReporting */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCrashReporting; + }; + 102B5EAE2E172F41003D191E /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogLogs; + }; + 102B5EB02E172F41003D191E /* DatadogRUM */ = { + isa = XCSwiftPackageProductDependency; + package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogRUM; + }; 25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */ = { isa = XCSwiftPackageProductDependency; productName = MeshtasticProtobufs; @@ -1992,6 +2077,9 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */, + DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */, + DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */, 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */, 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */, DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */, @@ -2043,7 +2131,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */; + currentVersion = DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/AppIntents/AddContactIntent.swift b/Meshtastic/AppIntents/AddContactIntent.swift new file mode 100644 index 00000000..e68ac4a3 --- /dev/null +++ b/Meshtastic/AppIntents/AddContactIntent.swift @@ -0,0 +1,48 @@ +// +// AddContactIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 5/13/25. +// + +import AppIntents +import MeshtasticProtobufs + +struct AddContactIntent: AppIntent { + static var title: LocalizedStringResource = "Add Contact" + static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database" + + @Parameter(title: "Contact URL", description: "The URL for the node to add") + var contactUrl: URL + + // Define the function that performs the main logic + func perform() async throws -> some IntentResult { + // Ensure the BLE Manager is connected + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") { + let components = self.contactUrl.absoluteString.components(separatedBy: "#") + // Extract contact information from the URL + if let contactData = components.last { + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) + if !success { + throw AppIntentErrors.AppIntentError.message("Failed to add contact") + } + + } catch { + throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)") + } + } + } + // Return a success result + return .result() + } else { + throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link") + } + } +} diff --git a/Meshtastic/AppIntents/DisconnectNodeIntent.swift b/Meshtastic/AppIntents/DisconnectNodeIntent.swift new file mode 100644 index 00000000..4f3b4b33 --- /dev/null +++ b/Meshtastic/AppIntents/DisconnectNodeIntent.swift @@ -0,0 +1,30 @@ +// +// DisconnectNodeIntent.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 4/2/25. +// + +import Foundation +import AppIntents + +struct DisconnectNodeIntent: AppIntent { + static var title: LocalizedStringResource = "Disconnect Node" + + static var description: IntentDescription = "Disconnect the currently connected node" + + func perform() async throws -> some IntentResult { + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if let connectedPeripheral = BLEManager.shared.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + BLEManager.shared.disconnectPeripheral(reconnect: false) + } else { + throw AppIntentErrors.AppIntentError.message("Error disconnecting node") + } + + return .result() + } +} diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift index 60946b00..9994c19d 100644 --- a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -11,11 +11,19 @@ import AppIntents struct FactoryResetNodeIntent: AppIntent { static var title: LocalizedStringResource = "Factory Reset" static var description: IntentDescription = "Perform a factory reset on the node you are connected to" + + @Parameter(title: "Hard Reset", description: "In addition to Config, Keys and BLE bonds will be wiped", default: false) + var hardReset: Bool + + @Parameter(title: "Provide Confirmation", description: "Show a confirmation dialog before performing the factory reset", default: true) + var provideConfirmation: Bool func perform() async throws -> some IntentResult { // Request user confirmation before performing the factory reset - try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName - .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + if provideConfirmation { + try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName + .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + } // Ensure the node is connected if !BLEManager.shared.isConnected { @@ -29,7 +37,7 @@ struct FactoryResetNodeIntent: AppIntent { let toUser = connectedNode.user { // Attempt to send a factory reset command, throw an error if it fails - if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) { + if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser, resetDevice: hardReset) { throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset") } } else { diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift index 7ae8095a..bff6affb 100644 --- a/Meshtastic/AppIntents/RestartNodeIntent.swift +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -15,7 +15,6 @@ struct RestartNodeIntent: AppIntent { func perform() async throws -> some IntentResult { - try await requestConfirmation(result: .result(dialog: "Reboot Node?")) if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected @@ -24,11 +23,10 @@ struct RestartNodeIntent: AppIntent { if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), let fromUser = connectedNode.user, - let toUser = connectedNode.user, - let adminIndex = connectedNode.myInfo?.adminIndex { + let toUser = connectedNode.user { // Attempt to send shutdown, throw an error if it fails - if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { + if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser) { throw AppIntentErrors.AppIntentError.message("Failed to restart") } } else { diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index 4352c548..fb0f97c3 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -11,6 +11,8 @@ import AppIntents import MeshtasticProtobufs struct SendWaypointIntent: AppIntent { + + var defaultDate = Date.now.addingTimeInterval(60 * 480) static var title = LocalizedStringResource("Send a Waypoint") @@ -23,13 +25,24 @@ struct SendWaypointIntent: AppIntent { @Parameter(title: "Emoji", default: "📍") var emojiParameter: String? - @Parameter(title: "Location") - var locationParameter: CLPlacemark + // Replace CLPlacemark with latitude and longitude parameters + @Parameter(title: "Latitude", description: "Latitude in degrees (e.g., 37.7749)") + var latitudeParameter: Double + + @Parameter(title: "Longitude", description: "Longitude in degrees (e.g., -122.4194)") + var longitudeParameter: Double + + @Parameter(title: "Locked", default: false) + var isLocked: Bool + + @Parameter(title: "Expiration") + var expiration: Date? func perform() async throws -> some IntentResult { if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected } + // Provide default values if parameters are nil let name = nameParameter ?? "Dropped Pin" let description = descriptionParameter ?? "" @@ -50,24 +63,39 @@ struct SendWaypointIntent: AppIntent { throw $emojiParameter.needsValueError("Must be a single emoji") } + // Validate latitude and longitude + guard abs(latitudeParameter) <= 90 else { + throw $latitudeParameter.needsValueError("Latitude must be between -90 and 90 degrees") + } + guard abs(longitudeParameter) <= 180 else { + throw $longitudeParameter.needsValueError("Longitude must be between -180 and 180 degrees") + } + var newWaypoint = Waypoint() - if let latitude = locationParameter.location?.coordinate.latitude { - newWaypoint.latitudeI = Int32(latitude * 10_000_000) - } - - if let longitude = locationParameter.location?.coordinate.longitude { - newWaypoint.longitudeI = Int32(longitude * 10_000_000) - } + // Set latitude and longitude directly + newWaypoint.latitudeI = Int32(latitudeParameter * 10_000_000) + newWaypoint.longitudeI = Int32(longitudeParameter * 10_000_000) newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - // This regex pattern is for matching a single emoji let emojiPattern = "^([\\p{So}\\p{Cn}])$" let regex = try? NSRegularExpression(pattern: emojiPattern, options: []) let matches = regex?.matches(in: emoji, options: [], range: NSRange(location: 0, length: emoji.utf16.count)) - return matches?.count == 1 } } diff --git a/Meshtastic/AppIntents/ShortcutsProvider.swift b/Meshtastic/AppIntents/ShortcutsProvider.swift index b21c7e7d..d87c06b1 100644 --- a/Meshtastic/AppIntents/ShortcutsProvider.swift +++ b/Meshtastic/AppIntents/ShortcutsProvider.swift @@ -32,5 +32,12 @@ struct ShortcutsProvider: AppShortcutsProvider { "Send a \(.applicationName) group message"], shortTitle: "Group Message", systemImageName: "message") + AppShortcut(intent: DisconnectNodeIntent(), + phrases: ["Disconnect \(.applicationName) node", + "Disconnect my \(.applicationName) node", + "Disconnect from \(.applicationName)", + "Disconnect \(.applicationName)"], + shortTitle: "Disconnect", + systemImageName: "antenna.radiowaves.left.and.right.slash") } } diff --git a/Meshtastic/AppIntents/ShutDownNodeIntent.swift b/Meshtastic/AppIntents/ShutDownNodeIntent.swift index dcb43f3c..7f5acc57 100644 --- a/Meshtastic/AppIntents/ShutDownNodeIntent.swift +++ b/Meshtastic/AppIntents/ShutDownNodeIntent.swift @@ -24,11 +24,10 @@ struct ShutDownNodeIntent: AppIntent { if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num, let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), let fromUser = connectedNode.user, - let toUser = connectedNode.user, - let adminIndex = connectedNode.myInfo?.adminIndex { + let toUser = connectedNode.user { // Attempt to send shutdown, throw an error if it fails - if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) { + if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser) { throw AppIntentErrors.AppIntentError.message("Failed to shut down") } } else { diff --git a/Meshtastic/AppIntents/TracerouteIntent.swift b/Meshtastic/AppIntents/TracerouteIntent.swift new file mode 100644 index 00000000..99c19348 --- /dev/null +++ b/Meshtastic/AppIntents/TracerouteIntent.swift @@ -0,0 +1,27 @@ +import Foundation +import AppIntents + +struct TracerouteIntent: AppIntent { + static var title: LocalizedStringResource = "Send a Traceroute" + + static var description: IntentDescription = "Send a traceroute request to a certain Meshtastic node" + + @Parameter(title: "Node Number") + var nodeNumber: Int + + static var parameterSummary: some ParameterSummary { + Summary("Send traceroute to \(\.$nodeNumber)") + } + + func perform() async throws -> some IntentResult { + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if !BLEManager.shared.sendTraceRouteRequest(destNum: Int64(nodeNumber), wantResponse: true) { + throw AppIntentErrors.AppIntentError.message("Failed to send traceroute request") + } + + return .result() + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json new file mode 100644 index 00000000..4234e370 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heltec_mesh_pocket.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg new file mode 100644 index 00000000..1af4f5c6 --- /dev/null +++ b/Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json new file mode 100644 index 00000000..d1ac2a26 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "seeed_solar.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg new file mode 100644 index 00000000..3f2b5d47 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json new file mode 100644 index 00000000..7c46aedd --- /dev/null +++ b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "progress.ring.dashed.svg", + "idiom" : "universal" + } + ] +} diff --git a/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg new file mode 100644 index 00000000..5d91388c --- /dev/null +++ b/Meshtastic/Assets.xcassets/progress.ring.dashed.symbolset/progress.ring.dashed.svg @@ -0,0 +1,169 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.6.0 + Requires Xcode 16 or greater + Generated from progress.ring.dashed + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Enums/KeyBackupStatus.swift b/Meshtastic/Enums/KeyBackupStatus.swift new file mode 100644 index 00000000..ff5b2438 --- /dev/null +++ b/Meshtastic/Enums/KeyBackupStatus.swift @@ -0,0 +1,47 @@ +// +// iCloudStats.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/18/25. +// + +enum KeyBackupStatus: String, CaseIterable, Equatable, Decodable { + case saved + case restored + case deleted + case saveFailed + case restoreFailed + case deleteFailed + var description: String { + switch self { + case .saved: + return "Private Key saved successfully to iCloud keychain.".localized + case .restored: + return "Private Key restored successfully from iCloud keychain.".localized + case .deleted: + return "Private Key deleted successfully from iCloud keychain.".localized + case .saveFailed: + return "Private Key failed to save to iCloud keychain.".localized + case .restoreFailed: + return "Private Key value not found in iCloud keychain.".localized + case .deleteFailed: + return "Private Key failed to delete from iCloud keychain.".localized + } + } + var success: Bool { + switch self { + case .saved: + return true + case .restored: + return true + case .deleted: + return true + case .saveFailed: + return false + case .restoreFailed: + return false + case .deleteFailed: + return false + } + } +} diff --git a/Meshtastic/Enums/RouteEnums.swift b/Meshtastic/Enums/RouteEnums.swift index 06876b4a..95b9c642 100644 --- a/Meshtastic/Enums/RouteEnums.swift +++ b/Meshtastic/Enums/RouteEnums.swift @@ -37,17 +37,17 @@ enum ActivityType: Int, CaseIterable, Identifiable { var fileNameString: String { switch self { case .walking: - return "walk".localized + return "Walking".localized.lowercased() case .hiking: - return "hiking".localized + return "Hiking".localized.lowercased() case .biking: - return "biking".localized + return "Biking".localized.lowercased() case .driving: - return "driving".localized + return "Driving".localized.lowercased() case .overlanding: - return "overlanding".localized + return "Overlanding".localized.lowercased() case .skiing: - return "skiing".localized + return "Skiing".localized.lowercased() } } } diff --git a/Meshtastic/Enums/SerialConfigEnums.swift b/Meshtastic/Enums/SerialConfigEnums.swift index 681ef89e..131b3d26 100644 --- a/Meshtastic/Enums/SerialConfigEnums.swift +++ b/Meshtastic/Enums/SerialConfigEnums.swift @@ -174,7 +174,7 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable { case .tenSeconds: return "Ten Seconds".localized case .fifteenSeconds: - return "Thirty Seconds".localized + return "Fifteen Seconds".localized case .thirtySeconds: return "Thirty Seconds".localized case .oneMinute: diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 57babf4a..c85eef4a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -32,6 +32,7 @@ extension ChannelEntity { channel.settings.psk = self.psk ?? Data() channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision) + channel.settings.moduleSettings.isClientMuted = self.mute return channel } } diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 70f36c3d..e7abb191 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -31,4 +31,11 @@ extension MessageEntity { return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() } + + func displayTimestamp(aboveMessage: MessageEntity?) -> Bool { + if let aboveMessage = aboveMessage { + return aboveMessage.timestamp.addingTimeInterval(3600) < timestamp // 60 minutes + } + return false // First message will have no timestamp + } } diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift new file mode 100644 index 00000000..034651be --- /dev/null +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift @@ -0,0 +1,30 @@ +// NodeInfoEntityToNodeInfo.swift +// Meshtastic +// +// Utility to convert NodeInfoEntity (Core Data) to NodeInfo (protobuf) + +import Foundation +import MeshtasticProtobufs + +extension NodeInfoEntity { + func toProto() -> NodeInfo { + var userProto = User() + if let user = self.user { + userProto.id = user.userId ?? "" + userProto.longName = user.longName ?? "" + userProto.shortName = user.shortName ?? "" + userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset + userProto.isLicensed = user.isLicensed + if userProto.hasIsUnmessagable == true { + userProto.isUnmessagable = user.unmessagable + } + userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client + userProto.publicKey = user.publicKey?.subdata(in: 0.. UserEntity { - let newUser = UserEntity(context: context) - newUser.num = Int64(num) - let userId = String(format: "%2X", num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" + +public func createUser(num: Int64, context: NSManagedObjectContext) throws -> UserEntity { + // Validate Input + guard num >= 0 else { + throw CoreDataError.invalidInput(message: "User number cannot be negative.") + } + + var newUser: UserEntity! // Use an implicitly unwrapped optional, but ensure it's assigned + + context.performAndWait { + newUser = UserEntity(context: context) + newUser.num = num + let userId = num.toHex() + newUser.userId = userId + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + } + return newUser } + +enum CoreDataError: Error, LocalizedError { + case invalidInput(message: String) + case saveFailed(message: String) + case entityCreationFailed(message: String) // In case UserEntity(context:) fails for some reason + + var errorDescription: String? { + switch self { + case .invalidInput(let message): + return "Core Data Input Error: \(message)" + case .saveFailed(let message): + return "Core Data Save Error: \(message)" + case .entityCreationFailed(let message): + return "Core Data Entity Creation Error: \(message)" + } + } +} diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 2f538b62..4f2923eb 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -14,9 +14,6 @@ extension WaypointEntity { static func allWaypointssFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = WaypointEntity.fetchRequest() request.fetchLimit = 50 - // request.fetchBatchSize = 1 - // request.returnsObjectsAsFaults = false - // request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)] request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate) @@ -24,7 +21,6 @@ extension WaypointEntity { } var latitude: Double? { - let d = Double(latitudeI) if d == 0 { return 0 @@ -33,7 +29,6 @@ extension WaypointEntity { } var longitude: Double? { - let d = Double(longitudeI) if d == 0 { return 0 @@ -46,7 +41,7 @@ extension WaypointEntity { let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) return coord } else { - return nil + return nil } } @@ -60,16 +55,29 @@ extension WaypointEntity { } extension WaypointEntity: MKAnnotation { - public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationsHandler.DefaultLocation } - public var title: String? { name ?? "Dropped Pin" } + @MainActor + public var coordinate: CLLocationCoordinate2D { + get { + waypointCoordinate ?? LocationsHandler.DefaultLocation + } + set { + latitudeI = Int32(newValue.latitude * 1e7) + longitudeI = Int32(newValue.longitude * 1e7) + } + } + + public var title: String? { + name ?? "Dropped Pin" + } + public var subtitle: String? { (longDescription ?? "") + String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") + - String(locked > 0 ? "\n🔒 Locked" : "") } + String(locked > 0 ? "\n🔒 Locked" : "") + } } struct WaypointCoordinate: Identifiable { - let id: UUID let coordinate: CLLocationCoordinate2D? let waypointId: Int64 diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index d2ae1e5a..9ade6991 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,6 +115,17 @@ extension String { .joined() } + /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver + /// This ensures proper pronunciation of alphanumeric node IDs + func formatNodeNameForVoiceOver() -> String { + let spaced = self.replacingOccurrences( + of: #"([A-Za-z])([0-9]+)"#, + with: "$1 $2", + options: .regularExpression + ) + return "Node".localized + " " + spaced + } + // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index cdea2fa4..0b124ac5 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -57,6 +57,7 @@ extension UserDefaults { case enableMapTraffic case enableMapPointsOfInterest case enableOfflineMaps + case enableMapShowFavorites case mapTileServer case enableOverlayServer case mapOverlayServer @@ -75,6 +76,7 @@ extension UserDefaults { case mapReportingOptIn case firstLaunch case showDeviceOnboarding + case usageDataAndCrashReporting case testIntEnum } @@ -121,6 +123,9 @@ extension UserDefaults { @UserDefault(.enableMapPointsOfInterest, defaultValue: false) static var enableMapPointsOfInterest: Bool + @UserDefault(.enableMapShowFavorites, defaultValue: false) + static var enableMapShowFavorites: Bool + @UserDefault(.enableDetectionNotifications, defaultValue: false) static var enableDetectionNotifications: Bool @@ -154,6 +159,8 @@ extension UserDefaults { @UserDefault(.mapReportingOptIn, defaultValue: false) static var mapReportingOptIn: Bool + @UserDefault(.usageDataAndCrashReporting, defaultValue: true) + static var usageDataAndCrashReporting: Bool @UserDefault(.firstLaunch, defaultValue: true) static var firstLaunch: Bool diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index dda84f99..4900eca2 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -32,10 +32,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public var isConnecting: Bool = false public var isConnected: Bool = false public var isSubscribed: Bool = false + public var allowDisconnect: Bool = false private var configNonce: UInt32 = 1 var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? + var maintenanceTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false @@ -52,6 +54,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2") let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") + @AppStorage("purgeStaleNodeDays") var purgeStaleNodeDays: Double = 0 + + let NONCE_ONLY_CONFIG = 69420 + let NONCE_ONLY_DB = 69421 + private var isWaitingForWantConfigResponse = false + + private var wantConfigTimer: Timer? + private var wantConfigRetryCount = 0 + private let maxWantConfigRetries = 6 + private let wantConfigTimeoutInterval: TimeInterval = 6.0 // MARK: init private override init() { @@ -68,13 +80,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } private init(appState: AppState, context: NSManagedObjectContext) { - self.appState = appState - self.context = context - self.lastConnectionError = "" - self.connectedVersion = "0.0.0" - super.init() - centralManager = CBCentralManager(delegate: self, queue: nil) - mqttManager.delegate = self + self.appState = appState + self.context = context + self.lastConnectionError = "" + self.connectedVersion = "0.0.0" + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + mqttManager.delegate = self + // Run clearStaleNodes every hour + maintenanceTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true, block: { _ in + let result = clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + // If you are connected and the clear worked, pull nodes back from the node in case we have deleted anything from that app that is in the device nodedb + if result && self.isSubscribed { + self.sendWantConfig() + } + }) } // MARK: Scanning for BLE Devices @@ -163,6 +183,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate isConnecting = false isConnected = false isSubscribed = false + allowDisconnect = false self.connectedPeripheral = nil invalidVersion = false connectedVersion = "0.0.0" @@ -177,20 +198,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { - - guard let connectedPeripheral = connectedPeripheral else { return } - if mqttProxyConnected { - mqttManager.mqttClientProxy?.disconnect() + // Ensure all operations run on the main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + guard let connectedPeripheral = self.connectedPeripheral else { return } + if self.mqttProxyConnected { + self.mqttManager.mqttClientProxy?.disconnect() + } + self.isWaitingForWantConfigResponse = false + if wantConfigTimer != nil { + self.wantConfigTimer?.invalidate() + } + self.wantConfigTimer = nil + self.wantConfigRetryCount = 0 + self.automaticallyReconnect = reconnect + self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) + self.FROMRADIO_characteristic = nil + self.isConnected = false + self.isSubscribed = false + self.allowDisconnect = false + self.invalidVersion = false + self.connectedVersion = "0.0.0" + self.stopScanning() + self.startScanning() } - automaticallyReconnect = reconnect - centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) - FROMRADIO_characteristic = nil - isConnected = false - isSubscribed = false - invalidVersion = false - connectedVersion = "0.0.0" - stopScanning() - startScanning() } // Called each time a peripheral is connected @@ -242,6 +273,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + resetWantConfigRetries() self.connectedPeripheral = nil self.isConnecting = false self.isConnected = false @@ -405,7 +437,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // MARK: Protobuf Methods - func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { + func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, context: NSManagedObjectContext) -> Int64 { guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return 0 } @@ -416,7 +448,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.priority = MeshPacket.Priority.reliable - meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true var dataMessage = DataMessage() if let serializedData: Data = try? adminPacket.serializedData() { @@ -468,7 +499,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let traceRoute = TraceRouteEntity(context: context) let nodes = NodeInfoEntity.fetchRequest() - nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral.num]) + if let connectedNum = self.connectedPeripheral?.num { + nodes.predicate = NSPredicate(format: "num IN %@", [destNum, connectedNum]) + } else { + nodes.predicate = NSPredicate(format: "num == %@", destNum) + } do { let fetchedNodes = try context.fetch(nodes) let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) @@ -494,31 +529,88 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - func sendWantConfig() { - guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } + func sendWantConfig() { + isWaitingForWantConfigResponse = true - if FROMRADIO_characteristic == nil { - Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)") - invalidVersion = true + guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } + + if FROMRADIO_characteristic == nil { + Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)") + invalidVersion = true + return + } else { + // Send Heartbeat before wantConfig + var heartbeatToRadio: ToRadio = ToRadio() + heartbeatToRadio.payloadVariant = .heartbeat(Heartbeat()) + guard let heartbeatBinaryData: Data = try? heartbeatToRadio.serializedData() else { + Logger.mesh.error("Failed to serialize Heartbeat ToRadio message") return - } else { - - let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized - let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) - Logger.mesh.info("🛎️ \(logString, privacy: .public)") - // BLE Characteristics discovered, issue wantConfig - var toRadio: ToRadio = ToRadio() - configNonce += 1 - toRadio.wantConfigID = configNonce - guard let binaryData: Data = try? toRadio.serializedData() else { - return - } - connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - // Either Read the config complete value or from num notify value - guard connectedPeripheral != nil else { return } - connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) } - } + connectedPeripheral!.peripheral.writeValue(heartbeatBinaryData, for: TORADIO_characteristic, type: .withResponse) + + let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized + let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName) + Logger.mesh.info("🛎️ \(logString, privacy: .public)") + // BLE Characteristics discovered, issue wantConfig + var toRadio: ToRadio = ToRadio() + configNonce = UInt32(NONCE_ONLY_DB) + if !isSubscribed { + configNonce = UInt32(NONCE_ONLY_CONFIG) // Get config first + } + toRadio.wantConfigID = configNonce + guard let binaryData: Data = try? toRadio.serializedData() else { + return + } + connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + // Either Read the config complete value or from num notify value + guard connectedPeripheral != nil else { return } + connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) + // Start timeout timer + startWantConfigTimeout() + } + } + + private func startWantConfigTimeout() { + // Cancel any existing timer + wantConfigTimer?.invalidate() + // Start new timer + wantConfigTimer = Timer.scheduledTimer(withTimeInterval: wantConfigTimeoutInterval, repeats: false) { [weak self] _ in + self?.handleWantConfigTimeout() + } + } + + private func handleWantConfigTimeout() { + guard isWaitingForWantConfigResponse else { return } + wantConfigRetryCount += 1 + if wantConfigRetryCount == 1 { + allowDisconnect = true + } + if wantConfigRetryCount < maxWantConfigRetries { + Logger.mesh.warning("⏰ Want Config timeout, retrying... (attempt \(self.wantConfigRetryCount + 1)/\(self.maxWantConfigRetries))") + sendWantConfig() + } else { + Logger.mesh.error("🚨 Want Config failed after \(self.maxWantConfigRetries) attempts, forcing disconnect") + lastConnectionError = "Bluetooth connection timeout, keep your node closer or reboot your radio if the problem continues.".localized + disconnectPeripheral(reconnect: false) + } + } + + func onWantConfigResponseReceived() { + if isWaitingForWantConfigResponse { + isWaitingForWantConfigResponse = false + wantConfigTimer?.invalidate() + wantConfigTimer = nil + wantConfigRetryCount = 0 // Reset retry count on success + } + } + + // Call this to reset the retry mechanism (e.g., on new connection) + func resetWantConfigRetries() { + wantConfigRetryCount = 0 + wantConfigTimer?.invalidate() + wantConfigTimer = nil + isWaitingForWantConfigResponse = false + } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error { @@ -637,6 +729,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { + var path = "meshtastic:///settings/debugLogs" if decodedInfo.clientNotification.hasReplyID { /// Set Sent bool on TraceRouteEntity to false if we got rate limited if decodedInfo.clientNotification.message.starts(with: "TraceRoute") { @@ -651,20 +744,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") } } + if decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.lowEntropyKey(decodedInfo.clientNotification.lowEntropyKey) || + decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.duplicatedPublicKey(decodedInfo.clientNotification.duplicatedPublicKey) { + path = "meshtastic:///settings/security" + } } let manager = LocalNotificationManager() manager.notifications = [ Notification( id: UUID().uuidString, - title: "Firmware Notification", + title: "Firmware Notification".localized, subtitle: "\(decodedInfo.clientNotification.level)".capitalized, content: decodedInfo.clientNotification.message, target: "settings", - path: "meshtastic:///settings/debugLogs" + path: path ) ] manager.schedule() - Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure")") + Logger.data.error("⚠️ Client Notification: \(decodedInfo.clientNotification.message, privacy: .public)") } switch decodedInfo.packet.decoded.portnum { @@ -691,6 +788,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // NodeInfo if decodedInfo.nodeInfo.num > 0 { + onWantConfigResponseReceived() nowKnown = true if let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context) { if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo.num { @@ -707,17 +805,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context) } // Config - if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { + if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil && self.connectedPeripheral?.num != 0 { nowKnown = true - localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) + localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { + onWantConfigResponseReceived() nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName) + moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) + if let connectedNum = self.connectedPeripheral?.num, connectedNum > 0 { + _ = self.getCannedMessageModuleMessages(destNum: connectedNum, wantResponse: true) + } } } } @@ -771,7 +872,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .nodeinfoApp: if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: - if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral.num, context: context) } + if !invalidVersion { + guard let peripheral = self.connectedPeripheral else { + Logger.mesh.error("🕸️ connectedPeripheral is nil. Unable to determine connectedNodeNum for routingPacket.") + return + } + routingPacket(packet: decodedInfo.packet, connectedNodeNum: peripheral.num, context: context) + } case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context) case .replyApp: @@ -940,7 +1047,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate subtitle: "TR received back from \(destinationHop.name ?? "unknown")", content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "Unknown".localized)\n\(tr.routeBackText ?? "Unknown".localized)", target: "nodes", - path: "meshtastic:///nodes?nodenum=\(connectedNode.user?.num ?? 0)" + path: "meshtastic:///nodes?nodenum=\(tr.node?.num ?? 0)" ) ] manager.schedule() @@ -975,12 +1082,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + case .keyVerificationApp: + Logger.mesh.warning("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } - if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_CONFIG { invalidVersion = false lastConnectionError = "" isSubscribed = true + allowDisconnect = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") if UserDefaults.firstLaunch { UserDefaults.showDeviceOnboarding = true @@ -1020,7 +1130,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } catch { Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription, privacy: .public)") } + Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") + sendWantConfig() + } + // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer @@ -1034,6 +1148,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return } + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_DB { + Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") + } case FROMNUM_UUID: Logger.services.info("🗞️ [BLE] (Notify) characteristic value will be read next") @@ -1072,7 +1189,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate success = false } else { - let fromUserNum: Int64 = self.connectedPeripheral.num + guard let fromUserNum = self.connectedPeripheral?.num else { + Logger.mesh.error("🚫 Connected peripheral user number is nil, cannot send message.") + return false + } let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) @@ -1123,6 +1243,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if newMessage.toUser?.pkiEncrypted ?? false { meshPacket.pkiEncrypted = true meshPacket.publicKey = newMessage.toUser?.publicKey ?? Data() + // Auto Favorite nodes you DM so they don't roll out of the nodedb + if !(newMessage.toUser?.userNode?.favorite ?? true) { + newMessage.toUser?.userNode?.favorite = true + do { + try context.save() + if let connectedPeripheral = self.connectedPeripheral { + Logger.data.info("💾 Auto favorited node based on sending a message \(connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + } else { + Logger.data.warning("⚠️ connectedPeripheral is nil while attempting to log auto-favoriting a node.") + } + guard let userNode = newMessage.toUser?.userNode else { + Logger.data.warning("⚠️ Unable to set favorite node: userNode is nil.") + return false + } + _ = self.setFavoriteNode(node: userNode, connectedNodeNum: fromUserNum) + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Unresolved Core Data error when auto favoriting in Send Message Function. Error: \(nsError, privacy: .public)") + } + } } meshPacket.id = UInt32(newMessage.messageId) if toUserNum > 0 { @@ -1152,7 +1293,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("💬 \(logString, privacy: .public)") do { try context.save() - Logger.data.info("💾 Saved a new sent message from \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + Logger.data.info("💾 Saved a new sent message from \(self.connectedPeripheral?.num.toHex() ?? "0", privacy: .public) to \(toUserNum.toHex(), privacy: .public)") success = true } catch { @@ -1163,14 +1304,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } } catch { - Logger.data.error("💥 Send message failure \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") + Logger.data.error("💥 Send message failure \(self.connectedPeripheral?.num.toHex() ?? "0", privacy: .public) to \(toUserNum.toHex(), privacy: .public)") } } return success } public func sendWaypoint(waypoint: Waypoint) -> Bool { - if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { + if waypoint.latitudeI == 0 && waypoint.longitudeI == 0 { return false } var success = false @@ -1380,6 +1521,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } public func sendTime() -> Bool { + if self.connectedPeripheral?.num ?? 0 <= 0 { + Logger.mesh.error("🚫 Unable to send time, connected node is disconnected or invalid") + return false + } var adminPacket = AdminMessage() adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) var meshPacket: MeshPacket = MeshPacket() @@ -1404,7 +1549,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } - public func sendShutdown(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + public func sendShutdown(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 if fromUser != toUser { @@ -1416,7 +1561,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func sendReboot(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 if fromUser != toUser { @@ -1444,7 +1588,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 if fromUser != toUser { @@ -1472,7 +1615,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity, resetDevice: Bool = false) -> Bool { var adminPacket = AdminMessage() - adminPacket.factoryResetConfig = 5 + if resetDevice { + adminPacket.factoryResetDevice = 5 + } else { + adminPacket.factoryResetConfig = 5 + } if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } @@ -1639,7 +1785,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } - public func saveChannelSet(base64UrlString: String, addChannels: Bool = false) -> Bool { + public func saveChannelSet(base64UrlString: String, addChannels: Bool = false, okToMQTT: Bool = false) -> Bool { if isConnected { var i: Int32 = 0 @@ -1721,6 +1867,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Save the LoRa Config and the device will reboot var adminPacket = AdminMessage() adminPacket.setConfig.lora = channelSet.loraConfig + adminPacket.setConfig.lora.configOkToMqtt = okToMQTT // Preserve users okToMQTT choice var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedPeripheral.num) meshPacket.from = UInt32(connectedPeripheral.num) @@ -1760,7 +1907,71 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } - public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + public func addContactFromURL(base64UrlString: String) -> Bool { + if isConnected { + + let decodedString = base64UrlString.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact: SharedContact = try SharedContact(serializedBytes: decodedData) + var adminPacket = AdminMessage() + adminPacket.addContact = contact + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedPeripheral.num) + meshPacket.from = UInt32(connectedPeripheral.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config if fromUser != toUser { @@ -1769,7 +1980,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham if fromUser != toUser { @@ -1952,7 +2162,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveBluetoothConfig(config: Config.BluetoothConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.bluetooth = config if fromUser != toUser { @@ -1978,7 +2187,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveDeviceConfig(config: Config.DeviceConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.device = config @@ -2009,7 +2217,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config if fromUser != toUser { @@ -2037,9 +2244,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - if adminIndex > 0 { - meshPacket.channel = UInt32(adminIndex) - } meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.lora = config @@ -2068,7 +2272,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func savePositionConfig(config: Config.PositionConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.position = config @@ -2098,7 +2301,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func savePowerConfig(config: Config.PowerConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.power = config @@ -2129,7 +2331,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.network = config @@ -2162,7 +2363,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveSecurityConfig(config: Config.SecurityConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.security = config @@ -2195,7 +2395,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.ambientLighting = config @@ -2228,7 +2427,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config @@ -2260,7 +2458,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveCannedMessageModuleMessages(messages: String, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages @@ -2292,7 +2489,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveDetectionSensorModuleConfig(config: ModuleConfig.DetectionSensorConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.detectionSensor = config @@ -2326,7 +2522,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveExternalNotificationModuleConfig(config: ModuleConfig.ExternalNotificationConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.externalNotification = config @@ -2356,7 +2551,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func savePaxcounterModuleConfig(config: ModuleConfig.PaxcounterConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.paxcounter = config @@ -2387,7 +2581,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) - meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveRtttlConfig(ringtone: String, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setRingtoneMessage = ringtone @@ -2420,7 +2613,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveMQTTConfig(config: ModuleConfig.MQTTConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.mqtt = config @@ -2452,7 +2644,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveRangeTestModuleConfig(config: ModuleConfig.RangeTestConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.rangeTest = config @@ -2484,7 +2675,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveSerialModuleConfig(config: ModuleConfig.SerialConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.serial = config @@ -2516,7 +2706,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveStoreForwardModuleConfig(config: ModuleConfig.StoreForwardConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.storeForward = config @@ -2547,7 +2736,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + public func saveTelemetryModuleConfig(config: ModuleConfig.TelemetryConfig, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.telemetry = config @@ -2578,7 +2766,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestBluetoothConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig @@ -2683,7 +2870,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestDeviceConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig @@ -2713,7 +2899,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestDisplayConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig @@ -2743,7 +2928,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestLoRaConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig @@ -2773,7 +2957,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestNetworkConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig @@ -2805,7 +2988,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestPositionConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig @@ -2834,7 +3016,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestPowerConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.powerConfig @@ -2863,7 +3044,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestSecurityConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.securityConfig @@ -2892,7 +3072,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig @@ -2921,7 +3100,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig @@ -2950,7 +3128,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestExternalNotificationModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig @@ -2979,7 +3156,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestPaxCounterModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig @@ -3008,7 +3184,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestRtttlConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true @@ -3037,7 +3212,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestRangeTestModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig @@ -3066,7 +3240,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestMqttModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig @@ -3095,7 +3268,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestDetectionSensorModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig @@ -3124,7 +3296,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig @@ -3153,7 +3324,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestStoreAndForwardModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig @@ -3182,7 +3352,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + public func requestTelemetryModuleConfig(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig @@ -3212,7 +3381,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. OSStatus { + let data = value.data(using: .utf8)! + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecValueData as String: data, + kSecAttrSynchronizable as String: kCFBooleanTrue!, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked + ] + + SecItemDelete(query as CFDictionary) // Delete existing item if any + + let status = SecItemAdd(query as CFDictionary, nil) + return status + } + + func read(key: String, service: String = Bundle.main.bundleIdentifier!) -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecReturnData as String: kCFBooleanTrue, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecAttrSynchronizable as String: kCFBooleanTrue! + ] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + if status == errSecSuccess { + if let data = item as? Data { + return String(data: data, encoding: .utf8) + } + } + return nil + } + + func delete(key: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: key, + kSecAttrSynchronizable as String: kCFBooleanTrue! + ] + + let status = SecItemDelete(query as CFDictionary) + return status + } +} diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index c51a2af3..6cad10a5 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -70,8 +70,8 @@ class LocalNotificationManager { if notification.critical { content.sound = UNNotificationSound.defaultCritical } - - let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error { diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 62cbd841..beaefe17 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -123,15 +123,42 @@ import OSLog } else { locationsArray = [location] } + UserDefaults.standard.set(location.coordinate.latitude, forKey: "lastKnownLatitude") + UserDefaults.standard.set(location.coordinate.longitude, forKey: "lastKnownLongitude") + UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastKnownLocationTimestamp") return true } - static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) static var currentLocation: CLLocationCoordinate2D { - guard let location = shared.manager.location else { + if let location = shared.manager.location { + return location.coordinate + } else { + // Check authorization status + let status = shared.manager.authorizationStatus + switch status { + case .notDetermined: + Logger.services.info("📍 [App] Location permission not determined, requesting authorization") + shared.manager.requestWhenInUseAuthorization() + case .denied, .restricted: + Logger.services.warning("📍 [App] Location access denied or restricted. Please enable location services in Settings to get accurate positioning!") + shared.manager.requestWhenInUseAuthorization() + default: + break + } + // Fallback 1: Last known location from UserDefaults (if within 4 hours) + if let lat = UserDefaults.standard.object(forKey: "lastKnownLatitude") as? Double, + let lon = UserDefaults.standard.object(forKey: "lastKnownLongitude") as? Double, + let timestamp = UserDefaults.standard.object(forKey: "lastKnownLocationTimestamp") as? Double, + lat >= -90 && lat <= 90, + lon >= -180 && lon <= 180, + Date().timeIntervalSince1970 - timestamp <= 14_400 { // 4 hours in seconds + Logger.services.info("📍 [App] Falling back to last known location (age: \(Int(Date().timeIntervalSince1970 - timestamp)) seconds)") + return CLLocationCoordinate2D(latitude: lat, longitude: lon) + } + // Fallback 2: Default location + Logger.services.warning("📍 [App] No Location and no last known location, something is really wrong. Teleporting user to Apple Park") return DefaultLocation } - return location.coordinate } static var satsInView: Int { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f5a7bda9..10a3d69b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -15,7 +15,7 @@ import OSLog import ActivityKit #endif -// Simple extension to consicely pass values through a has_XXX boolean check +// Simple extension to concisely pass values through a has_XXX boolean check fileprivate extension Bool { func then(_ value: T) -> T? { self ? value : nil @@ -185,9 +185,6 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo mutableChannels.add(newChannel) } fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet - if newChannel.name?.lowercased() == "admin" { - fetchedMyInfo[0].adminIndex = newChannel.index - } context.refresh(newChannel, mergeChanges: true) do { try context.save() @@ -292,14 +289,18 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newTelemetries.append(telemetry) newNode.telemetries? = NSOrderedSet(array: newTelemetries) } - - newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + if nodeInfo.lastHeard > 0 { + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() + } newNode.snr = nodeInfo.snr if nodeInfo.hasUser { let newUser = UserEntity(context: context) - newUser.userId = nodeInfo.user.id + newUser.userId = nodeInfo.num.toHex() newUser.num = Int64(nodeInfo.num) newUser.longName = nodeInfo.user.longName newUser.shortName = nodeInfo.user.shortName @@ -317,10 +318,27 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.pkiEncrypted = true newUser.publicKey = nodeInfo.user.publicKey } + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfo.user.hasIsUnmessagable { + newUser.unmessagable = nodeInfo.user.isUnmessagable + } else { + let roles = [2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(newUser.role)) + if containsRole { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + }} newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - newNode.user = newUser + do { + let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { @@ -380,26 +398,43 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user?.pkiEncrypted = true fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey } - fetchedNode[0].user!.userId = nodeInfo.user.id - fetchedNode[0].user!.num = Int64(nodeInfo.num) - fetchedNode[0].user!.numString = String(nodeInfo.num) - fetchedNode[0].user!.longName = nodeInfo.user.longName - fetchedNode[0].user!.shortName = nodeInfo.user.shortName - fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed - fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + fetchedNode[0].user?.userId = nodeInfo.num.toHex() + fetchedNode[0].user?.num = Int64(nodeInfo.num) + fetchedNode[0].user?.numString = String(nodeInfo.num) + fetchedNode[0].user?.longName = nodeInfo.user.longName + fetchedNode[0].user?.shortName = nodeInfo.user.shortName + fetchedNode[0].user?.isLicensed = nodeInfo.user.isLicensed + fetchedNode[0].user?.role = Int32(nodeInfo.user.role.rawValue) + fetchedNode[0].user?.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + fetchedNode[0].user?.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfo.user.hasIsUnmessagable { + fetchedNode[0].user?.unmessagable = nodeInfo.user.isUnmessagable + } else { + let roles = [-1, 2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) + if containsRole { + fetchedNode[0].user?.unmessagable = true + } else { + fetchedNode[0].user?.unmessagable = false + } + } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) - fetchedNode[0].user!.hwDisplayName = dh?.displayName + fetchedNode[0].user?.hwDisplayName = dh?.displayName } } } else { if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { - - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - fetchedNode[0].user = newUser + do { + let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + fetchedNode[0].user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } @@ -469,9 +504,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) { - - if !cmmc.messages.isEmpty { - let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex()) Logger.mesh.info("🥫 \(logString, privacy: .public)") @@ -485,10 +517,11 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { .replacingOccurrences(of: "11: ", with: "") .replacingOccurrences(of: "\"", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "\n").first ?? "" fetchedNode[0].cannedMessageConfig?.messages = messages do { try context.save() - Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num.toHex(), privacy: .public)") + Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, privacy: .public)") } catch { context.rollback() let nsError = error as NSError @@ -498,7 +531,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } catch { Logger.data.error("💥 Error Deserializing ADMIN_APP packet.") } - } } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) @@ -688,7 +720,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { let logString = String.localizedStringWithFormat("Telemetry received for: %@".localized, String(packet.from)) Logger.mesh.info("📈 \(logString, privacy: .public)") - if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { + if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { /// Other unhandled telemetry packets return } @@ -909,7 +941,14 @@ func textMessageAppPacket( // For S&F broadcast messages, treat as a channel message (not a DM) newMessage.toUser = nil } else { - newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + newMessage.toUser = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { @@ -939,7 +978,14 @@ func textMessageAppPacket( } } else { /// Make a new from user if they are unknown - newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + newMessage.fromUser = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) @@ -956,79 +1002,79 @@ func textMessageAppPacket( try context.save() Logger.data.info("💾 Saved a new message for \(newMessage.messageId, privacy: .public)") messageSaved = true - - if messageSaved { - if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { - return - } - if newMessage.fromUser != nil && newMessage.toUser != nil { - // Set Unread Message Indicators - if packet.to == connectedNode { - appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 - } - if !(newMessage.fromUser?.mute ?? false) { - // Create an iOS Notification for the received DM message - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(packet.from), - critical: critical - ) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") - } - } else if newMessage.fromUser != nil && newMessage.toUser == nil { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if !fetchedMyInfo.isEmpty { - appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { - // Create an iOS Notification for the received channel message - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(newMessage.fromUser?.userId ?? "0"), - critical: critical - ) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") - } - } - } - } catch { - // Handle error - } - } - } } catch { context.rollback() let nsError = error as NSError Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)") } + // Send notifications if the message saved properly to core data + if messageSaved { + if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { + return + } + if newMessage.fromUser != nil && newMessage.toUser != nil { + // Set Unread Message Indicators + if packet.to == connectedNode { + appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 + } + if !(newMessage.fromUser?.mute ?? false) { + // Create an iOS Notification for the received DM message + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(packet.from), + critical: critical + ) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") + } + } else if newMessage.fromUser != nil && newMessage.toUser == nil { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if !fetchedMyInfo.isEmpty { + appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { + // Create an iOS Notification for the received channel message + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0"), + critical: critical + ) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") + } + } + } + } catch { + // Handle error + } + } + } } catch { Logger.data.error("Fetch Message To and From Users Error") } @@ -1040,17 +1086,29 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from)) Logger.mesh.info("📍 \(logString, privacy: .public)") - let fetchWaypointRequest = WaypointEntity.fetchRequest() - fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id)) - do { - if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { - let fetchedWaypoint = try context.fetch(fetchWaypointRequest) - if fetchedWaypoint.isEmpty { - let waypoint = WaypointEntity(context: context) + // Fetch waypoint by waypointMessage.id, not packet.id + let fetchWaypointRequest = WaypointEntity.fetchRequest() + fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id)) - waypoint.id = Int64(packet.id) + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) + // Fetch the node info to get the short name + var nodeShortName: String = "?" + let fetchNodeRequest = NodeInfoEntity.fetchRequest() + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + do { + let fetchedNode = try context.fetch(fetchNodeRequest) + if let node = fetchedNode.first, let user = node.user { + nodeShortName = user.shortName ?? node.user?.userId ?? String(packet.from.toHex()) + } + } catch { + Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)") + } + if fetchedWaypoint.isEmpty { + // Create a new waypoint + let waypoint = WaypointEntity(context: context) + waypoint.id = Int64(waypointMessage.id) // Use waypointMessage.id waypoint.name = waypointMessage.name waypoint.longDescription = waypointMessage.description_p waypoint.latitudeI = waypointMessage.latitudeI @@ -1073,7 +1131,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { manager.notifications = [ Notification( id: ("notification.id.\(waypoint.id)"), - title: "New Waypoint Received", + title: "New Waypoint From \(nodeShortName)", subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")", content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")", target: "map", @@ -1088,26 +1146,42 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") } } else { - fetchedWaypoint[0].id = Int64(packet.id) - fetchedWaypoint[0].name = waypointMessage.name - fetchedWaypoint[0].longDescription = waypointMessage.description_p - fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI - fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI - fetchedWaypoint[0].icon = Int64(waypointMessage.icon) - fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo) - if waypointMessage.expire >= 1 { - fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } else { - fetchedWaypoint[0].expire = nil - } - fetchedWaypoint[0].lastUpdated = Date() - do { - try context.save() - Logger.data.info("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id, privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + // Update existing waypoint + let existingWaypoint = fetchedWaypoint[0] + if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from { + let currentTime = Int64(Date().timeIntervalSince1970) + if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime { + context.delete(existingWaypoint) + do { + try context.save() + Logger.data.info("💾 Deleted a waypoint") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } + } else { + existingWaypoint.name = waypointMessage.name + existingWaypoint.longDescription = waypointMessage.description_p + existingWaypoint.latitudeI = waypointMessage.latitudeI + existingWaypoint.longitudeI = waypointMessage.longitudeI + existingWaypoint.icon = Int64(waypointMessage.icon) + existingWaypoint.locked = Int64(waypointMessage.lockedTo) + if waypointMessage.expire >= 1 { + existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + existingWaypoint.expire = nil + } + existingWaypoint.lastUpdated = Date() + do { + try context.save() + Logger.data.info("💾 Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } + } } } } diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index cf21d92e..62b9dc4b 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -41,31 +41,29 @@ class MqttClientProxyManager { if let host = host { let port = defaultServerPort - let username = node.mqttConfig?.username - let password = node.mqttConfig?.password let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh" let prefix = root! topic = prefix + "/2/e" + "/#" // Require opt in to map report terms to connect if node.mqttConfig?.mapReportingEnabled ?? false && UserDefaults.mapReportingOptIn || !(node.mqttConfig?.mapReportingEnabled ?? false) { - connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic) + connect(host: host, port: port, useSsl: useSsl, topic: topic, node: node) } else { delegate?.onMqttError(message: "MQTT Map Reporting Terms need to be accepted.") } } } - func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?) { + func connect(host: String, port: Int, useSsl: Bool, topic: String?, node: NodeInfoEntity) { guard !host.isEmpty else { delegate?.onMqttDisconnected() return } - let clientId = "MeshtasticAppleMqttProxy-" + String(ProcessInfo().processIdentifier) + let clientId = "MeshtasticAppleMqttProxy-" + (node.user?.userId ?? String(ProcessInfo().processIdentifier)) mqttClientProxy = CocoaMQTT(clientID: clientId, host: host, port: UInt16(port)) if let mqttClient = mqttClientProxy { mqttClient.enableSSL = useSsl mqttClient.allowUntrustCACertificate = true - mqttClient.username = username - mqttClient.password = password + mqttClient.username = node.mqttConfig?.username + mqttClient.password = node.mqttConfig?.password mqttClient.keepAlive = 60 mqttClient.cleanSession = true if debugLog { diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index abcef61d..3689ef3e 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -2,12 +2,15 @@ - com.apple.developer.usernotifications.critical-alerts - com.apple.developer.associated-domains applinks:meshtastic.org/e/* + applinks:meshtastic.org/v/* + com.apple.developer.carplay-communication + + com.apple.developer.usernotifications.critical-alerts + com.apple.developer.weatherkit com.apple.security.app-sandbox @@ -20,7 +23,9 @@ com.apple.security.personal-information.location - com.apple.developer.carplay-communication - + keychain-access-groups + + $(AppIdentifierPrefix)gvh.MeshtasticClient + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index cd530ab2..057ab601 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 50.xcdatamodel + MeshtasticDataModelV 53.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents index 9578507a..1124c59b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 50.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents new file mode 100644 index 00000000..f6d43c72 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 51.xcdatamodel/contents @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents new file mode 100644 index 00000000..c36266d8 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents new file mode 100644 index 00000000..c1ee4271 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 53.xcdatamodel/contents @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 0145a7a7..9ce07142 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -4,14 +4,17 @@ import SwiftUI import CoreData import OSLog import TipKit +import MeshtasticProtobufs +import DatadogCore +import DatadogCrashReporting +import DatadogRUM @main struct MeshtasticAppleApp: App { - @UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) - private var appDelegate + @UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) private var appDelegate - @ObservedObject var appState: AppState + @ObservedObject var appState: AppState private let persistenceController: PersistenceController @@ -26,37 +29,71 @@ struct MeshtasticAppleApp: App { let appState = AppState( router: Router() ) - self._appState = ObservedObject(wrappedValue: appState) + // Initialize Datadog + // RUM Client Tokens are NOT secret + let appID = "79fe92a9-74c9-4c8f-ba63-6308384ecfa9" + let clientToken = "pub4427bea20dbdb08a6af68034de22cd3b" + let environment = "testflight" + Datadog.initialize( + with: Datadog.Configuration( + clientToken: clientToken, + env: environment, + site: .us5 + ), + trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted + ) + + RUM.enable( + with: RUM.Configuration( + applicationID: appID, + uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(), + uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate() + ) + ) + self._appState = ObservedObject(wrappedValue: appState) // Initialize the BLEManager singleton with the necessary dependencies BLEManager.setup(appState: appState, context: persistenceController.container.viewContext) self.persistenceController = persistenceController - // Wire up router self.appDelegate.router = appState.router - // Show Tips + #if DEBUG + // Show tips in development try? Tips.resetDatastore() + #endif } - var body: some Scene { WindowGroup { ContentView( appState: appState, router: appState.router ) - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(appState) - .environmentObject(BLEManager.shared) - .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: BLEManager.shared) - .presentationDetents([.large]) - .presentationDragIndicator(.visible) + .sheet(isPresented: Binding( + get: { + saveChannels && !(channelSettings == nil) + }, + set: { newValue in + saveChannels = newValue + if !newValue { + channelSettings = nil + } + } + )) { + SaveChannelQRCode( + channelSetLink: channelSettings ?? "Empty Channel URL", + addChannels: addChannels, + bleManager: BLEManager.shared + ) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in Logger.mesh.debug("URL received \(userActivity, privacy: .public)") self.incomingUrl = userActivity.webpageURL - - if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil { + self.saveChannels = false + if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true { + ContactURLHandler.handleContactUrl(url: self.incomingUrl!, bleManager: BLEManager.shared) + } else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/") == true { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil { @@ -72,7 +109,7 @@ struct MeshtasticAppleApp: App { } Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") } - self.saveChannels = true + self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") } if self.saveChannels { @@ -80,10 +117,11 @@ struct MeshtasticAppleApp: App { } } .onOpenURL(perform: { (url) in - Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url - if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) + } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { @@ -99,7 +137,7 @@ struct MeshtasticAppleApp: App { } Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") } - self.saveChannels = true + self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link", privacy: .public)") } else if url.absoluteString.lowercased().contains("meshtastic:///") { appState.router.route(url: url) @@ -115,7 +153,7 @@ struct MeshtasticAppleApp: App { .displayFrequency(.immediate) ] ) - } + } } .onChange(of: scenePhase) { (_, newScenePhase) in switch newScenePhase { @@ -138,5 +176,9 @@ struct MeshtasticAppleApp: App { Logger.services.error("🍎 [App] Apple must have changed something") } } + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(appState) + .environmentObject(BLEManager.shared) } + } diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 365faef4..f4c405bb 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -97,7 +97,22 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if let targetValue = userInfo["target"] as? String, let deepLink = userInfo["path"] as? String, let url = URL(string: deepLink) { - Logger.services.info("userNotificationCenter didReceiveResponse \(targetValue, privacy: .public) \(deepLink, privacy: .public)") + Logger.services.info("userNotificationCenter didReceiveResponse handling deeplink: \(targetValue, privacy: .public) \(deepLink, privacy: .public)") + // Handle TraceRoute notifications specially to ensure they navigate correctly + if deepLink.contains("meshtastic:///nodes") && deepLink.contains("nodenum=") { + // First extract the node number from the URL + if let nodeNumString = deepLink.components(separatedBy: "nodenum=").last, + let nodeNum = Int64(nodeNumString) { + Logger.services.info("Navigation to specific node via notification: \(nodeNum, privacy: .public)") + self.router?.navigationState.selectedTab = .nodes + // Post a notification to trigger app-wide refresh + NotificationCenter.default.post(name: NSNotification.Name("ForceNavigationRefresh"), + object: nil, + userInfo: ["nodeNum": nodeNum]) + self.router?.navigationState.nodeListSelectedNodeNum = nodeNum + } + } + // Still call the regular router in all cases router?.route(url: url) } else { Logger.services.error("Failed to handle notification response: \(userInfo, privacy: .public)") diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index b1892fa9..bc0093a8 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,6 +8,57 @@ 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) + } + var nodePKIExpireTime: TimeInterval { + return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400) + } + + if nodeExpireDays == 0 { + // Purge Disabled + Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes") + return false + } + let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))", + NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime)) + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + batchDeleteRequest.resultType = .resultTypeCount + + do { + Logger.data.info("💾 [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days") + if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult { + try context.save() + let deletedNodes = batchDeleteResult.result as? Int ?? 0 + Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + if deletedNodes > 0 { + return true + } + } else { + Logger.data.error("💥 [NodeInfoEntity] bad delete results") + } + } catch { + context.rollback() + Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes") + } + return false +} + public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() @@ -160,7 +211,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.channel = Int32(packet.channel) } if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { - newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + if nodeInfoMessage.hasHopsAway { + newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + } newNode.favorite = nodeInfoMessage.isFavorite } @@ -168,19 +221,37 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if newUserMessage.id.isEmpty { if packet.from > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(packet.from), context: context) - newNode.user = newUser + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } else { let newUser = UserEntity(context: context) - newUser.userId = newUserMessage.id + newUser.userId = newNode.num.toHex() newUser.num = Int64(packet.from) newUser.longName = newUserMessage.longName newUser.shortName = newUserMessage.shortName newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if newUserMessage.hasIsUnmessagable { + newUser.unmessagable = newUserMessage.isUnmessagable + } else { + let roles = [2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(newUser.role)) + if containsRole { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + } + } if !newUserMessage.publicKey.isEmpty { newUser.pkiEncrypted = true newUser.publicKey = newUserMessage.publicKey @@ -211,17 +282,34 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } else { if packet.from > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(packet.from), context: context) - if !packet.publicKey.isEmpty { - newNode.user?.pkiEncrypted = true - newNode.user?.publicKey = packet.publicKey + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + if !packet.publicKey.isEmpty { + newNode.user?.pkiEncrypted = true + newNode.user?.publicKey = packet.publicKey + } + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") } - newNode.user = newUser } } - + // User is messed up and has failed to create at least once, if this fails bail out if newNode.user == nil && packet.from > Constants.minimumNodeNum { - newNode.user = createUser(num: Int64(packet.from), context: context) + do { + let newUser = try createUser(num: Int64(packet.from), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + context.rollback() + return + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + context.rollback() + return + } } let myInfoEntity = MyInfoEntity(context: context) @@ -269,22 +357,33 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { - /// Seeing Some crashes here ? - fetchedNode[0].user!.userId = nodeInfoMessage.user.id - fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) - fetchedNode[0].user!.longName = nodeInfoMessage.user.longName - fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName - fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() - fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex() + fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) + fetchedNode[0].user?.longName = nodeInfoMessage.user.longName + fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName + fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue) + fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfoMessage.user.hasIsUnmessagable { + fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable + } else { + let roles = [-1, 2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) + if containsRole { + fetchedNode[0].user?.unmessagable = true + } else { + fetchedNode[0].user?.unmessagable = false + } + } if !nodeInfoMessage.user.publicKey.isEmpty { - fetchedNode[0].user!.pkiEncrypted = true - fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) - fetchedNode[0].user!.hwDisplayName = dh?.displayName + fetchedNode[0].user?.hwDisplayName = dh?.displayName } } } @@ -292,9 +391,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if fetchedNode[0].user == nil { - let newUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) - fetchedNode[0].user? = newUser - + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + fetchedNode[0].user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } do { try context.save() @@ -358,12 +462,12 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } - guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { + guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else { return } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { - if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 15.0 { + if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 { mutablePositions.remove(mostRecent) } } else if mutablePositions.count > 0 { @@ -528,6 +632,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses newDisplayConfig.displayMode = Int32(config.displaymode.rawValue) newDisplayConfig.units = Int32(config.units.rawValue) newDisplayConfig.headingBold = config.headingBold + newDisplayConfig.use12HClock = config.use12HClock fetchedNode[0].displayConfig = newDisplayConfig } else { @@ -539,6 +644,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue) fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue) fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) + fetchedNode[0].displayConfig?.use12HClock = config.use12HClock fetchedNode[0].displayConfig?.headingBold = config.headingBold } if sessionPasskey != nil { @@ -687,7 +793,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - let logString = String.localizedStringWithFormat("Positon config received: %@".localized, String(nodeNum)) + let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum)) Logger.data.info("🗺️ \(logString, privacy: .public)") let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() @@ -1205,6 +1311,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 newMQTTConfig.jsonEnabled = config.jsonEnabled newMQTTConfig.tlsEnabled = config.tlsEnabled newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled + newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) fetchedNode[0].mqttConfig = newMQTTConfig @@ -1273,6 +1380,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod do { try context.save() Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { context.rollback() let nsError = error as NSError @@ -1404,23 +1512,23 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod if !fetchedNode.isEmpty { if fetchedNode[0].telemetryConfig == nil { let newTelemetryConfig = TelemetryConfigEntity(context: context) - newTelemetryConfig.deviceUpdateInterval = Int32(config.deviceUpdateInterval) - newTelemetryConfig.environmentUpdateInterval = Int32(config.environmentUpdateInterval) + newTelemetryConfig.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) + newTelemetryConfig.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled - newTelemetryConfig.powerUpdateInterval = Int32(config.powerUpdateInterval) + newTelemetryConfig.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig } else { - fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(config.deviceUpdateInterval) - fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(config.environmentUpdateInterval) + fetchedNode[0].telemetryConfig?.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) + fetchedNode[0].telemetryConfig?.environmentUpdateInterval = safeInt32(from: 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 = Int32(config.powerUpdateInterval) + fetchedNode[0].telemetryConfig?.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } if sessionPasskey != nil { diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index bbe99f2a..4163ebaa 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -229,7 +229,8 @@ "images": [ "tlora-t3s3-epaper.svg" ], - "requiresDfu": true + "requiresDfu": true, + "hasInkHud": true }, { "hwModel": 17, @@ -604,7 +605,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (small TFT)", "requiresDfu": true, @@ -626,7 +627,7 @@ "hwModelSlug": "HELTEC_WIRELESS_TRACKER", "platformioTarget": "tracksenger-oled", "architecture": "esp32-s3", - "activelySupported": false, + "activelySupported": true, "supportLevel": 3, "displayName": "TrackSenger (big OLED)", "partitionScheme": "8MB" @@ -872,5 +873,110 @@ "images": [ "thinknode_m2.svg" ] + }, + { + "hwModel": 94, + "hwModelSlug": "HELTEC_MESH_POCKET", + "platformioTarget": "heltec-mesh-pocket-10000", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Heltec MeshPocket", + "tags": [ + "Heltec" + ], + "images": [ + "heltec_mesh_pocket.svg" + ], + "requiresDfu": true, + "hasInkHud": true + }, + { + "hwModel": 95, + "hwModelSlug": "SEEED_SOLAR_NODE", + "platformioTarget": "seeed_solar_node", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed SenseCAP Solar Node", + "tags": [ + "Seeed" + ], + "images": [ + "seeed_solar.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 99, + "hwModelSlug": "SEEED_WIO_TRACKER_L1", + "platformioTarget": "seeed_wio_tracker_L1", + "architecture": "nrf52840", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Seeed Wio Tracker L1", + "tags": [ + "Seeed" + ], + "images": [ + "wio_tracker_l1_case.svg" + ], + "requiresDfu": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv1-43-50-70-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 4.3/5.0/7.0 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_5_0.svg", + "crowpanel_7_0.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv-24-28-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 2.4/2.8 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_2_4.svg", + "crowpanel_2_8.svg" + ], + "partitionScheme": "16MB", + "hasMui": true + }, + { + "hwModel": 97, + "hwModelSlug": "CROWPANEL", + "platformioTarget": "elecrow-adv-35-tft", + "architecture": "esp32-s3", + "activelySupported": true, + "supportLevel": 1, + "displayName": "Crowpanel Adv 3.5 TFT", + "tags": [ + "Elecrow" + ], + "requiresDfu": true, + "images": [ + "crowpanel_3_5.svg" + ], + "partitionScheme": "16MB", + "hasMui": true } ] diff --git a/Meshtastic/Tips/BluetoothTips.swift b/Meshtastic/Tips/BluetoothTips.swift index 838d29fc..72c22108 100644 --- a/Meshtastic/Tips/BluetoothTips.swift +++ b/Meshtastic/Tips/BluetoothTips.swift @@ -16,7 +16,7 @@ struct BluetoothConnectionTip: Tip { Text("Connected Radio") } var message: Text? { - Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity.") + Text("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.") } var image: Image? { Image(systemName: "flipphone") diff --git a/Meshtastic/Tips/PersistantTips.swift b/Meshtastic/Tips/PersistantTips.swift new file mode 100644 index 00000000..4cf9858b --- /dev/null +++ b/Meshtastic/Tips/PersistantTips.swift @@ -0,0 +1,37 @@ +// +// Untitled.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 6/19/25. +// +import TipKit + +struct PersistentTip: TipViewStyle { + func makeBody(configuration: Configuration) -> some View { + VStack { + HStack(alignment: .top) { + if let image = configuration.image { + image + .font(.system(size: 42)) + .foregroundColor(.accentColor) + .padding(.trailing, 5) + } + VStack(alignment: .leading) { + if let title = configuration.title { + title + .bold() + .font(.headline) + } + if let message = configuration.message { + message + .foregroundStyle(.secondary) + .font(.callout) + } + } + } + } + .frame(maxWidth: .infinity) + .backgroundStyle(.thinMaterial) + .padding() + } +} diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 152c6b69..284eba62 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -32,9 +32,10 @@ struct Connect: View { VStack { List { if bleManager.isSwitchedOn { - Section(header: Text("Connected Radio").font(.title)) { + Section { if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected { TipView(BluetoothConnectionTip(), arrowEdge: .bottom) + .tipViewStyle(PersistentTip()) VStack(alignment: .leading) { HStack { VStack(alignment: .center) { @@ -76,47 +77,58 @@ struct Connect: View { .foregroundColor(Color.gray) .padding([.top]) .swipeActions { - Button(role: .destructive) { - if let connectedPeripheral = bleManager.connectedPeripheral, - connectedPeripheral.peripheral.state == .connected { - bleManager.disconnectPeripheral(reconnect: false) + if bleManager.allowDisconnect { + Button(role: .destructive) { + if let connectedPeripheral = bleManager.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + bleManager.disconnectPeripheral(reconnect: false) + } + } label: { + Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } - } label: { - Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } } .contextMenu { if node != nil { + Label("\(String(node!.num))", systemImage: "number") + Label("BLE RSSI \(connectedPeripheral.rssi)", systemImage: "cellularbars") #if !targetEnvironment(macCatalyst) - Button { - if !liveActivityStarted { - #if canImport(ActivityKit) - Logger.services.info("Start live activity.") - startNodeActivity() - #endif - } else { + if bleManager.isSubscribed { + Button { + if !liveActivityStarted { #if canImport(ActivityKit) - Logger.services.info("Stop live activity.") - endActivity() - #endif + Logger.services.info("Start live activity.") + startNodeActivity() + #endif + } else { + #if canImport(ActivityKit) + Logger.services.info("Stop live activity.") + endActivity() + #endif + } + } label: { + Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") } - } label: { - Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") } #endif - Text("Num: \(String(node!.num))") - Text("Short Name: \(node?.user?.shortName ?? "?")") - Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") - Text("BLE RSSI: \(connectedPeripheral.rssi)") - - Button { - if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) { - Logger.mesh.error("Shutdown Failed") + if bleManager.allowDisconnect { + Button(role: .destructive) { + if let connectedPeripheral = bleManager.connectedPeripheral, + connectedPeripheral.peripheral.state == .connected { + bleManager.disconnectPeripheral(reconnect: false) + } + } label: { + Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } + Button(role: .destructive) { + if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) { + Logger.mesh.error("Shutdown Failed") + } - } label: { - Label("Power Off", systemImage: "power") + } label: { + Label("Power Off", systemImage: "power") + } } } } @@ -132,7 +144,6 @@ struct Connect: View { } } } else { - if bleManager.isConnecting { HStack { Image(systemName: "antenna.radiowaves.left.and.right") diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index c5d17f16..2e8d5c53 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,47 +32,64 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - let signalStrength: BLESignalStrength + // Accessibility: VoiceOver description + private var accessibilityDescription: String { + switch signalStrength { + case .weak: + return "Signal strength weak".localized + case .normal: + return "Signal strength normal".localized + case .strong: + return "Signal strength strong".localized + } + } - var body: some View { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - } - } - } + let signalStrength: BLESignalStrength - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + var body: some View { + Group { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Signal strength".localized) + .accessibilityValue(accessibilityDescription) + } + + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 4ac61d0c..f04a5c99 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,69 +13,101 @@ struct BatteryCompact: View { var color: Color var body: some View { + // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - if batteryLevel == 100 { - Image(systemName: "battery.100.bolt") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 100 && batteryLevel > 74 { - Image(systemName: "battery.75") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 75 && batteryLevel > 49 { - Image(systemName: "battery.50") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 50 && batteryLevel > 14 { - Image(systemName: "battery.25") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel < 15 && batteryLevel > 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } else if batteryLevel == 0 { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(.red) - .symbolRenderingMode(.multicolor) - } else if batteryLevel > 100 { + // Check for plugged in state + let isPluggedIn = batteryLevel > 100 + let isCharging = batteryLevel == 100 + // Battery icon selection based on level + if isPluggedIn { Image(systemName: "powerplug") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) // Hide from VoiceOver since container will handle it + } else if isCharging { + Image(systemName: "battery.100.bolt") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 74 { + Image(systemName: "battery.75") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 49 { + Image(systemName: "battery.50") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 14 { + Image(systemName: "battery.25") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else if batteryLevel > 0 { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) + } else { + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(.red) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) } - } else { - Image(systemName: "battery.0") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - } - if let batteryLevel { - if batteryLevel > 100 { + // Battery text label + if isPluggedIn { Text("PWD") .foregroundStyle(.secondary) .font(font) - } else if batteryLevel == 100 { + .accessibilityHidden(true) + } else if isCharging { Text("CHG") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } else { Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } else { + // Unknown battery state + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) + .accessibilityHidden(true) Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) + .accessibilityHidden(true) } } + // Setup container-level accessibility for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + // Set appropriate value based on the battery state using a computed property + .accessibilityValue(batteryLevel.map { level in + if level > 100 { + // Plugged in - same as PWD visual indicator + return "Plugged in".localized + } else if level == 100 { + // Charging - same as CHG visual indicator + return "Charging".localized + } else { + // Normal battery level + return String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(level)) + } + } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 952c9768..addbc97c 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,18 +18,20 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) + // For VoiceOver purposes, detect when device is plugged in (battery > 100%) + let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 + // Use a capped battery level for UI display + let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) VStack { - if batteryLevel > 100.0 { - // Plugged in - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) + if isPluggedIn { + // Use a completely standalone view for the plugged in state + // to avoid any VoiceOver confusion + PluggedInIndicator() } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { + // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -50,6 +52,8 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } + .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) + .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -63,6 +67,23 @@ struct BatteryGauge: View { } } +/// A dedicated view for showing a device is plugged in +/// With proper VoiceOver support that matches the visual indication +struct PluggedInIndicator: View { + var body: some View { + // This view is isolated from any battery measurement + // to ensure VoiceOver doesn't pick up any percentages + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + // Override the accessibility to ensure correct VoiceOver announcement + .accessibilityElement(children: .ignore) + .accessibilityLabel("Battery Level".localized) + .accessibilityValue("Plugged in".localized) + } +} + struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift new file mode 100644 index 00000000..3a66dc5a --- /dev/null +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -0,0 +1,35 @@ +// +// ChannelLock.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/22/25. +// +import SwiftUI + +struct ChannelLock: View { + + @ObservedObject var channel: ChannelEntity + + var body: some View { + /// Unencrypted - using no key at all or a known 1 byte key + if channel.psk?.hexDescription.count ?? 0 < 3 { + let preciseLoction = 17...32 + // Using precise location and have MQTT uplink enabled + if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .foregroundColor(.red) + // Using precise location + } else if preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.fill") + .foregroundColor(.red) + // Just unencrypted without any location or MQTT + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } +} diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index c795b1b0..e0dc8a02 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,22 +21,46 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) + // Create an HStack for connected state with proper accessibility + HStack { + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + .accessibilityHidden(true) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + Text(name.addingVariationSelectors) + .font(name.isEmoji() ? .title : .callout) + .foregroundColor(.gray) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Connected to Bluetooth device".localized + ", " + name.formatNodeNameForVoiceOver()) } else { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) + // Create a container for disconnected state + HStack { + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("No Bluetooth device connected".localized) } } else { - Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) + // Create a container for Bluetooth off state + HStack { + Text("bluetooth.off".localized) + .font(.subheadline) + .foregroundColor(.red) + .accessibilityHidden(true) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("bluetooth.off".localized) } } } diff --git a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift new file mode 100644 index 00000000..ad8b3b06 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift @@ -0,0 +1,97 @@ +// +// ChannelHelp.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/18/25. +// + +import SwiftUI + +struct ChannelsHelp: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Environment(\.dismiss) private var dismiss + + var body: some View { + ScrollView { + Label("Channels Help", systemImage: "questionmark.circle") + .font(.title) + .padding(.vertical) + VStack(alignment: .leading) { + HStack { + CircleText(text: String(0), color: .accentColor) + .brightness(0.2) + .offset(y: -10) + Text("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.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + .padding(.leading, 7) + } + HStack { + Image(systemName: "lock.fill") + .padding(.leading) + .padding(.trailing, 7) + .foregroundColor(Color.green) + .font(.title) + Text("A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.fill") + .padding(.leading) + .foregroundColor(Color.yellow) + .font(.title) + Text("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.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.fill") + .padding(.leading) + .foregroundColor(Color.red) + .font(.title) + Text("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.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .padding(.leading) + .symbolRenderingMode(.multicolor) + .foregroundColor(Color.red) + .font(.title) + Text("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.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + } + +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("Close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .frame(minHeight: 0, maxHeight: .infinity, alignment: .leading) + .padding() + .presentationDetents([.large]) + .presentationContentInteraction(.scrolls) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) + } +} + +struct ChannelHelpPreviews: PreviewProvider { + static var previews: some View { + VStack { + ChannelsHelp() + } + } +} diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index c207d084..2a8fc3e7 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -51,6 +51,20 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider { static var previews: some View { ScrollView { VStack { + VStack { + // Good + LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + // Fair + LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + // Bad + LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + // None + LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true) + .padding(.bottom) + }.padding() HStack { // Good LoRaSignalStrengthMeter(snr: -1, rssi: -114, preset: ModemPresets.longFast, compact: false) @@ -85,16 +99,5 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider { } .padding(.top) } - - VStack { - // Good - LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true) - // Fair - LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true) - // Bad - LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true) - // None - LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true) - } } } diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 84040d92..c6906f15 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -11,27 +11,22 @@ struct MeshtasticLogo: View { @Environment(\.colorScheme) var colorScheme var body: some View { - #if targetEnvironment(macCatalyst) VStack { Image("logo-white") .resizable() - .renderingMode(.template) .foregroundColor(.accentColor) .scaledToFit() } .padding(.bottom, 5) .padding(.top, 5) - .offset(x: -15) #else VStack { Image(colorScheme == .dark ? "logo-white" : "logo-black") .resizable() - .renderingMode(.template) .scaledToFit() } .padding(.bottom, 5) - .offset(x: -15) #endif } } diff --git a/Meshtastic/Views/Helpers/RateLimitedButton.swift b/Meshtastic/Views/Helpers/RateLimitedButton.swift new file mode 100644 index 00000000..30c5d667 --- /dev/null +++ b/Meshtastic/Views/Helpers/RateLimitedButton.swift @@ -0,0 +1,113 @@ +// +// RateLimitCountdownView.swift +// Meshtastic +// +// Created by Jake Bordens on 5/5/25. +// + +import SwiftUI + +// This class provides a rate limited button. +// Provide a key to differentiate which action is rate-limited +// This allows you to keep different rate limits for different action +// Rate limits are stored in a RateLimitStorage singleton, but do not persist +public struct RateLimitedButton: View { + typealias Builder = ((percentComplete: Double, secondsRemaining: TimeInterval)?) -> Content + + let key: String + + @StateObject var storage = RateLimitStorage.shared + + let rateLimit: TimeInterval + let content: Builder + let action: () -> Void + + init(key: String, rateLimit: TimeInterval, action: @escaping () -> Void, @ViewBuilder label: @escaping Builder) { + self.key = key + self.rateLimit = rateLimit + self.content = label + self.action = action + } + + public var body: some View { + let percentRemaining = storage.rateLimitRemainingPercentage(forKey: key) + let secondsRemaining = storage.rateLimitSecondsRemaining(forKey: key) + if percentRemaining > 0.0 { + content((percentRemaining, secondsRemaining)) + } else { + Button { + storage.actionOccured(forKey: key, rateLimit: rateLimit) + action() + } label: { + content(nil) + } + } + } +} + +// To store the time an action occured (name by a key) and the time limit +// Does not persist across app launches +class RateLimitStorage: ObservableObject { + private struct RateLimiter { + var actionOccuredTimestamp: Date + var rateLimitSeconds: TimeInterval + + var rateLimitExpires: Date { + return actionOccuredTimestamp.addingTimeInterval(rateLimitSeconds) + } + } + + static var shared: RateLimitStorage = RateLimitStorage() // Singleton instance + + private var rateLimits = [String: RateLimiter]() + private var timer: Timer? + + func actionOccured(forKey key: String, rateLimit: TimeInterval) { + let now = Date() + if let existingRateLimit = rateLimits[key] { + if existingRateLimit.rateLimitExpires > now.addingTimeInterval(rateLimit) { + // We have an existing rate limit that is larger than the one being requested + // Ignore + return + } + } + self.objectWillChange.send() + rateLimits[key] = RateLimiter(actionOccuredTimestamp: now, rateLimitSeconds: rateLimit) + startTimerIfNecessary() + } + + func rateLimitRemainingPercentage(forKey: String) -> Double { + guard let rateLimit = rateLimits[forKey] else { + return 0.0 + } + let percent = (rateLimit.rateLimitExpires.timeIntervalSinceNow) / rateLimit.rateLimitSeconds + return min(1.0, max(percent, 0.0)) + } + + func rateLimitSecondsRemaining(forKey: String) -> TimeInterval { + guard let rateLimit = rateLimits[forKey] else { + return 0.0 + } + return rateLimit.rateLimitExpires.timeIntervalSinceNow + } + + func startTimerIfNecessary() { + // Timer exists, don't create one + guard timer == nil else { return } + + // Create the timer + self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.objectWillChange.send() + + // Determine if we can clean up the dictionary and stop the timer. + let maxExpiration = self.rateLimits.values.map { $0.rateLimitExpires }.max() ?? .distantPast + if maxExpiration.timeIntervalSinceNow < 0 { + // All rateLimits are in the past. Stop and clean up + self.timer?.invalidate() + self.timer = nil + self.rateLimits.removeAll() + } + } + } +} diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index aaed8bd1..687cc6fe 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -12,19 +12,28 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String @Binding private var isValid: Bool - @State var isSecure: Bool = true private var title: String - init(_ title: String, text: Binding, isValid: Binding) { + // Local state to store the value of iSSecure, or optionally a binding + private var isSecureBinding: Binding? + @State private var isSecureLocal: Bool = true + + private var isSecure: Binding { + // Use the binding if we have one, otherwise fallback to the local state variable + isSecureBinding ?? $isSecureLocal + } + + init(_ title: String, text: Binding, isValid: Binding, isSecure: Binding? = nil) { self.title = title self._text = text self._isValid = isValid + self.isSecureBinding = isSecure } var body: some View { ZStack(alignment: .trailing) { Group { - if isSecure { + if isSecure.wrappedValue { SecureField(title, text: $text) .font(idiom == .phone ? .caption : .callout) .allowsTightening(true) @@ -51,9 +60,9 @@ struct SecureInput: View { if !text.isEmpty { Button(action: { - isSecure.toggle() + isSecure.wrappedValue.toggle() }) { - Image(systemName: self.isSecure ? "eye.slash" : "eye") + Image(systemName: self.isSecure.wrappedValue ? "eye.slash" : "eye") .accentColor(.secondary) }.buttonStyle(BorderlessButtonStyle()) } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 1123c4ab..835c662d 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -21,11 +21,17 @@ struct ChannelList: View { var channelSelection: ChannelEntity? @State private var isPresentingDeleteChannelMessagesConfirm: Bool = false - @State private var isPresentingTraceRouteSentAlert = false + @State private var showingHelp = false var restrictedChannels = ["gpio", "mqtt", "serial", "admin"] + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)], + predicate: nil, + animation: .default + ) private var channels: FetchedResults + @ViewBuilder private func makeChannelRow( myInfo: MyInfoEntity, @@ -52,6 +58,7 @@ struct ChannelList: View { VStack(alignment: .leading) { HStack { + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()) @@ -87,6 +94,9 @@ struct ChannelList: View { .foregroundColor(.secondary) } } + if channel.mute { + Image(systemName: "bell.slash") + } } if channel.allPrivateMessages.count > 0 { @@ -103,7 +113,7 @@ struct ChannelList: View { var body: some View { VStack { // Display Contacts for the rest of the non admin channels - if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] { + if let node, let myInfo = node.myInfo { List(selection: $channelSelection) { ForEach(channels) { (channel: ChannelEntity) in if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { @@ -119,7 +129,7 @@ struct ChannelList: View { } } Button { - channel.mute = !channel.mute + channel.mute.toggle() do { let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) if adminMessageId > 0 { @@ -152,8 +162,31 @@ struct ChannelList: View { } .padding([.top, .bottom]) .listStyle(.plain) + .navigationTitle("Channels") } } - .navigationTitle("Channels") + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 6093d9eb..42bd5c12 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -33,8 +33,15 @@ struct ChannelMessageList: View { ZStack(alignment: .bottomTrailing) { ScrollView { LazyVStack { - ForEach(channel.allPrivateMessages) { (message: MessageEntity) in + ForEach(Array(channel.allPrivateMessages.enumerated()), id: \.element.id) { index, message in + // Get the previous message, if it exists + let previousMessage = index > 0 ? channel.allPrivateMessages[index - 1] : nil let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false) + if message.displayTimestamp(aboveMessage: previousMessage) { + Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundColor(.gray) + } if message.replyID > 0 { let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) HStack { @@ -44,7 +51,6 @@ struct ChannelMessageList: View { messageToHighlight = messageNum } scrollView.scrollTo(messageNum, anchor: .center) - // Reset highlight after delay Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second @@ -130,12 +136,11 @@ struct ChannelMessageList: View { Spacer(minLength: 50) } } - - .overlay { - RoundedRectangle(cornerRadius: 10) - .stroke(.blue, lineWidth: 2) - .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) - } +// .overlay { +// RoundedRectangle(cornerRadius: 18) +// .stroke(.blue, lineWidth: 2) +// .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) +// } .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) @@ -175,24 +180,24 @@ struct ChannelMessageList: View { } .scrollDismissesKeyboard(.interactively) .onFirstAppear { - // Find first unread message - if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId { + if channel.unreadMessages == 0 { withAnimation { - scrollView.scrollTo(firstUnreadMessageId, anchor: .top) - showScrollToBottomButton = true + scrollView.scrollTo("bottomAnchor", anchor: .bottom) + hasReachedBottom = true } } else { - // If no unread messages, scroll to bottom - withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true + if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId { + withAnimation { + scrollView.scrollTo(firstUnreadMessageId, anchor: .top) + showScrollToBottomButton = true + } } } gotFirstUnreadMessage = true } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) hasReachedBottom = true showScrollToBottomButton = false } @@ -200,7 +205,7 @@ struct ChannelMessageList: View { .onChange(of: channel.allPrivateMessages) { if hasReachedBottom { withAnimation { - scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) } } else { showScrollToBottomButton = true diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index c8a994c3..ac033b1f 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -17,6 +17,10 @@ struct MessageText: View { let tapBackDestination: MessageDestination let isCurrentUser: Bool let onReply: () -> Void + // State for handling channel URL sheet + @State private var saveChannels = false + @State private var channelSettings: String? + @State private var addChannels = false @State private var isShowingDeleteConfirmation = false @@ -83,6 +87,60 @@ struct MessageText: View { onReply: onReply ) } + .environment(\.openURL, OpenURLAction { url in + channelSettings = nil + + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + // Handle contact URL + ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) + return .handled // Prevent default browser opening + } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { + // Handle channel URL + let components = url.absoluteString.components(separatedBy: "#") + guard !components.isEmpty, let lastComponent = components.last else { + Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") + return .discarded + } + + self.addChannels = Bool(url.query?.contains("add=true") ?? false) + guard let lastComponent = components.last else { + Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)") + self.channelSettings = nil + return .discarded + } + + self.channelSettings = lastComponent.components(separatedBy: "?").first ?? "" + + + Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)") + self.saveChannels = true + Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)") + return .handled // Prevent default browser opening + } + + return .systemAction // Open other URLs in browser +}) + + // Display sheet for channel settings + .sheet(isPresented: Binding( + get: { + saveChannels && !(channelSettings == nil) + }, + set: { newValue in + saveChannels = newValue + if !newValue { + channelSettings = nil + } + } + )) { + SaveChannelQRCode( + channelSetLink: channelSettings ?? "Empty Channel URL", + addChannels: addChannels, + bleManager: BLEManager.shared + ) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } .confirmationDialog( "Are you sure you want to delete this message?", isPresented: $isShowingDeleteConfirmation, diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index cb6947c0..8a75faf7 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -64,6 +64,7 @@ struct Messages: View { } TipView(MessagesTip(), arrowEdge: .top) + .tipViewStyle(PersistentTip()) } .navigationTitle("Messages") .navigationBarTitleDisplayMode(.large) diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index 2f1634bc..fac95a11 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,6 +6,7 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") + .accessibilityLabel("Position Exchange Requested".localized) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index d60a0381..945b41c0 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -39,8 +39,9 @@ struct TextMessageField: View { } label: { Image(systemName: "x.circle.fill") } - Text("Replying to a message") + Text("Reply") } + .padding(.top) } ZStack { diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index aacbd60d..c6a0032f 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,6 +6,8 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .accessibilityLabel(NSLocalizedString("Message Size", comment: "VoiceOver label for message size")) + .accessibilityValue(String(format: NSLocalizedString("Bytes Used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 7e57ce8c..6d20ec23 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -39,18 +39,6 @@ struct UserList: View { roleFilter ]} - @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), - NSSortDescriptor(key: "userNode.favorite", ascending: false), - NSSortDescriptor(key: "pkiEncrypted", ascending: false), - NSSortDescriptor(key: "userNode.lastHeard", ascending: false), - NSSortDescriptor(key: "longName", ascending: true)], - predicate: NSPredicate( - format: "userNode.ignored == false && longName != '' AND NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)" - ), animation: .default - ) - var users: FetchedResults - @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? @@ -60,8 +48,23 @@ struct UserList: View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { - List(selection: $userSelection) { - ForEach(users) { (user: UserEntity) in + FilteredUserList( + searchText: searchText, + viaLora: viaLora, + viaMqtt: viaMqtt, + isOnline: isOnline, + isPkiEncrypted: isPkiEncrypted, + isFavorite: isFavorite, + isIgnored: isIgnored, + isEnvironment: isEnvironment, + distanceFilter: distanceFilter, + maxDistance: maxDistance, + hopsAway: hopsAway, + roleFilter: roleFilter, + deviceRoles: deviceRoles, + userSelection: $userSelection + ) { users in + List(users, selection: $userSelection) { (user: UserEntity) in let mostRecent = user.messageList.last let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 @@ -134,18 +137,17 @@ struct UserList: View { .frame(height: 62) .contextMenu { Button { - if node != nil && !(user.userNode?.favorite ?? false) { let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) + user.userNode?.favorite = !(user.userNode?.favorite ?? false) Logger.data.info("Favorited a node") } } else { let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Un Favorited a node") + user.userNode?.favorite = !(user.userNode?.favorite ?? false) + Logger.data.info("Unfavorited a node") } } context.refresh(user, mergeChanges: true) @@ -156,7 +158,7 @@ struct UserList: View { Logger.data.error("Save Node Favorite Error") } } label: { - Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") + Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") } Button { user.mute = !user.mute @@ -169,7 +171,7 @@ struct UserList: View { } label: { Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") } - if user.messageList.count > 0 { + if user.messageList.count > 0 { Button(role: .destructive) { isPresentingDeleteUserMessagesConfirm = true userSelection = user @@ -192,66 +194,15 @@ struct UserList: View { } } } + .listStyle(.plain) + .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count))) } - .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count))) .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } .sheet(isPresented: $showingHelp) { DirectMessagesHelp() } - .onChange(of: searchText) { - Task { - await searchUserList() - } - } - .onChange(of: viaLora) { - if !viaLora && !viaMqtt { - viaMqtt = true - } - Task { - await searchUserList() - } - } - .onChange(of: viaMqtt) { - if !viaLora && !viaMqtt { - viaLora = true - } - Task { - await searchUserList() - } - } - .onChange(of: [deviceRoles]) { - Task { - await searchUserList() - } - } - .onChange(of: hopsAway) { - Task { - await searchUserList() - } - } - .onChange(of: [boolFilters]) { - Task { - await searchUserList() - } - } - .onChange(of: maxDistance) { - Task { - await searchUserList() - } - } - .onChange(of: isPkiEncrypted) { - Task { - await searchUserList() - } - } - .onAppear { - Task { - await searchUserList() - } - } .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { Button(action: { @@ -282,36 +233,60 @@ struct UserList: View { .padding(5) } .padding(.bottom, 5) - .padding(.bottom, 5) - .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) } } - private func searchUserList() async { +} - /// Case Insensitive Search Text Predicates - let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in - return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) - } - /// Create a compound predicate using each text search preicate as an OR - let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) - /// Create an array of predicates to hold our AND predicates +struct FilteredUserList: View { + @FetchRequest var fetchRequest: FetchedResults + let content: (FetchedResults) -> Content + + var body: some View { + content(fetchRequest) + } + + init( + searchText: String, + viaLora: Bool, + viaMqtt: Bool, + isOnline: Bool, + isPkiEncrypted: Bool, + isFavorite: Bool, + isIgnored: Bool, + isEnvironment: Bool, + distanceFilter: Bool, + maxDistance: Double, + hopsAway: Double, + roleFilter: Bool, + deviceRoles: Set, + userSelection: Binding, + @ViewBuilder content: @escaping (FetchedResults) -> Content + ) { + self.content = content + // Build predicates based on filter variables var predicates: [NSPredicate] = [] - /// Mqtt and lora + // Search text predicates + if !searchText.isEmpty { + let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) + } + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + predicates.append(textSearchPredicate) + } + // Mqtt and lora if !(viaLora && viaMqtt) { if viaLora { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") predicates.append(mqttPredicate) } - } else { - let mqttPredicate = NSPredicate(format: "NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)") - predicates.append(mqttPredicate) } - /// Roles + // Roles if roleFilter && deviceRoles.count > 0 { var rolesArray: [NSPredicate] = [] for dr in deviceRoles { @@ -321,33 +296,32 @@ struct UserList: View { let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: rolesArray) predicates.append(compoundPredicate) } - /// Hops Away - if hopsAway == 0.0 { + // Hops Away + if hopsAway == 0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } else if hopsAway > -1.0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway > 0 AND userNode.hopsAway <= %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } - /// Online + // Online if isOnline { let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } - /// Encrypted + // Encrypted if isPkiEncrypted { let isPkiEncryptedPredicate = NSPredicate(format: "pkiEncrypted == YES") predicates.append(isPkiEncryptedPredicate) } - /// Favorites + // Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - /// Distance + // Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation - if pointOfInterest.latitude != LocationsHandler.DefaultLocation.latitude && pointOfInterest.longitude != LocationsHandler.DefaultLocation.longitude { let d: Double = maxDistance * 1.1 let r: Double = 6371009 @@ -362,16 +336,29 @@ struct UserList: View { predicates.append(distancePredicate) } } - - if predicates.count > 0 || !searchText.isEmpty { - if !searchText.isEmpty { - let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) - } else { - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) - } - } else { - users.nsPredicate = nil - } + // Always apply unmessagable and connected node filters + // Show unmessagable nodes only if they have messages, otherwise hide them + let unmessagablePredicate = NSPredicate(format: "unmessagable == NO") + let hasMessagesPredicate = NSPredicate(format: "receivedMessages.@count > 0 OR sentMessages.@count > 0") + let isUnmessagablePredicate = NSCompoundPredicate(type: .or, subpredicates: [unmessagablePredicate, hasMessagesPredicate]) + predicates.append(isUnmessagablePredicate) + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") + predicates.append(isIgnoredPredicate) + let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", String(UserDefaults.preferredPeripheralNum)) + predicates.append(isConnectedNodePredicate) + // Combine all predicates + let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) + // Initialize the fetch request with the combined predicate + _fetchRequest = FetchRequest( + sortDescriptors: [ + NSSortDescriptor(key: "lastMessage", ascending: false), + NSSortDescriptor(key: "userNode.favorite", ascending: false), + NSSortDescriptor(key: "pkiEncrypted", ascending: false), + NSSortDescriptor(key: "userNode.lastHeard", ascending: false), + NSSortDescriptor(key: "longName", ascending: true) + ], + predicate: finalPredicate, + animation: .spring + ) } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 686decaf..7b27b4f2 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -14,7 +14,6 @@ struct UserMessageList: View { @EnvironmentObject var appState: AppState @EnvironmentObject var bleManager: BLEManager @Environment(\.managedObjectContext) var context - // Keyboard State @FocusState var messageFieldFocused: Bool // View State Items @@ -24,16 +23,22 @@ struct UserMessageList: View { @State private var showScrollToBottomButton = false @State private var hasReachedBottom = false @State private var gotFirstUnreadMessage: Bool = false - @State private var messageToHighlight: Int64 = 0 - + var body: some View { VStack { ScrollViewReader { scrollView in ZStack(alignment: .bottomTrailing) { ScrollView { LazyVStack { - ForEach( user.messageList ) { (message: MessageEntity) in + ForEach( Array(user.messageList.enumerated()), id: \.element.id) { index, message in + // Get the previous message, if it exists + let previousMessage = index > 0 ? user.messageList[index - 1] : nil + if message.displayTimestamp(aboveMessage: previousMessage) { + Text(message.timestamp.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundColor(.gray) + } if user.num != bleManager.connectedPeripheral?.num ?? -1 { let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false) @@ -46,7 +51,6 @@ struct UserMessageList: View { messageToHighlight = messageNum } scrollView.scrollTo(messageNum, anchor: .center) - // Reset highlight after delay Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second @@ -119,11 +123,11 @@ struct UserMessageList: View { Spacer(minLength: 50) } } - .overlay { - RoundedRectangle(cornerRadius: 10) - .stroke(.blue, lineWidth: 2) - .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) - } +// .overlay { +// RoundedRectangle(cornerRadius: 10) +// .stroke(.blue, lineWidth: 2) +// .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0) +// } .padding([.leading, .trailing]) .frame(maxWidth: .infinity) .id(message.messageId) @@ -163,24 +167,24 @@ struct UserMessageList: View { } .scrollDismissesKeyboard(.interactively) .onFirstAppear { - // Find first unread message - if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId { + if user.unreadMessages == 0 { withAnimation { - scrollView.scrollTo(firstUnreadMessageId, anchor: .top) - showScrollToBottomButton = true + scrollView.scrollTo("bottomAnchor", anchor: .bottom) + hasReachedBottom = true } } else { - // If no unread messages, scroll to bottom - withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) - hasReachedBottom = true + if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId { + withAnimation { + scrollView.scrollTo(firstUnreadMessageId, anchor: .top) + showScrollToBottomButton = true + } } } gotFirstUnreadMessage = true } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) hasReachedBottom = true showScrollToBottomButton = false } @@ -188,7 +192,7 @@ struct UserMessageList: View { .onChange(of: user.messageList) { if hasReachedBottom { withAnimation { - scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom) + scrollView.scrollTo("bottomAnchor", anchor: .bottom) } } else { showScrollToBottomButton = true diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 84fdf4d3..2d73d5c0 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,6 +40,7 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } + // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift index 64e2563a..95489e95 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -9,25 +9,28 @@ struct TraceRouteButton: View { private var isPresentingTraceRouteSentAlert: Bool = false var body: some View { - Button { + RateLimitedButton(key: "traceroute", rateLimit: 30.0) { isPresentingTraceRouteSentAlert = bleManager.sendTraceRouteRequest( destNum: node.user?.num ?? 0, wantResponse: true ) - } label: { - Label { - Text("Trace Route") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - } - }.alert( - "Trace Route Sent", - isPresented: $isPresentingTraceRouteSentAlert - ) { - Button("OK") { }.keyboardShortcut(.defaultAction) - } message: { - Text("This could take a while. The response will appear in the trace route log for the node it was sent to.") + } label: { completion in + if let completion, completion.percentComplete > 0.0 { + Label { + Text("Trace Route (in \(completion.secondsRemaining.formatted(.number.precision(.fractionLength(0))))s)") + .foregroundStyle(.secondary) + } icon: { + Image("progress.ring.dashed", variableValue: completion.percentComplete) + .foregroundStyle(.secondary) + }.disabled(true) + } else { + Label { + Text("Trace Route") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + } } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 4bce572c..832ae9c6 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -15,6 +15,7 @@ struct MeshMapContent: MapContent { @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @AppStorage("enableMapConvexHull") private var showConvexHull = false + @AppStorage("enableMapShowFavorites") private var showFavorites = false @Binding var showTraffic: Bool @Binding var showPointsOfInterest: Bool @Binding var selectedMapLayer: MapLayer @@ -39,11 +40,12 @@ struct MeshMapContent: MapContent { @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in - /// Node color from node.num - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - let positionName = position.nodePosition?.user?.longName ?? "?" - /// Latest Position Anotations - Annotation(positionName, coordinate: position.coordinate) { + if !showFavorites || (position.nodePosition?.favorite == true) { + /// Node color from node.num + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + let positionName = position.nodePosition?.user?.longName ?? "?" + /// Latest Position Anotations + Annotation(positionName, coordinate: position.coordinate) { LazyVStack { ZStack { let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) @@ -59,6 +61,13 @@ struct MeshMapContent: MapContent { .onAppear { self.scale = 1 } + .onChange(of: showFavorites) { + + scale = 0.5 // Reset to initial state + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + scale = 1 + } + } .frame(width: 60, height: 60) } if position.nodePosition?.hasDetectionSensorMetrics ?? false { @@ -130,7 +139,7 @@ struct MeshMapContent: MapContent { } } /// Reduced Precision Map Circles - if 10...19 ~= position.precisionBits { + if 12...15 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius: CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { @@ -141,6 +150,8 @@ struct MeshMapContent: MapContent { } } } + + } } @MapContentBuilder diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 2d157979..88d217ae 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -49,7 +49,7 @@ struct NodeMapContent: MapContent { let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(position.heading)) /// Reduced Precision Map Circle - if position.latest && 10...19 ~= position.precisionBits { + if position.latest && 12...15 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius: CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 85db444a..8a2ba6bc 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -12,9 +12,10 @@ struct MapSettingsForm: View { @Environment(\.dismiss) private var dismiss @State private var currentDetent = PresentationDetent.medium @AppStorage("meshMapShowNodeHistory") private var nodeHistory = false - @AppStorage("meshMapShowRouteLines") private var routeLines = false + @AppStorage("meshMapShowRouteLines") private var enableMapRouteLines = false @AppStorage("enableMapConvexHull") private var convexHull = false - @AppStorage("enableMapWaypoints") private var waypoints = true + @AppStorage("enableMapWaypoints") private var enableMapWaypoints = true + @AppStorage("enableMapShowFavorites") private var enableMapShowFavorites = false @Binding var traffic: Bool @Binding var pointsOfInterest: Bool @Binding var mapLayer: MapLayer @@ -29,7 +30,7 @@ struct MapSettingsForm: View { Picker(selection: $mapLayer, label: Text("")) { ForEach(MapLayer.allCases, id: \.self) { layer in if layer != MapLayer.offline { - Text(layer.localized) + Text(layer.localized.capitalized) } } } @@ -53,15 +54,25 @@ struct MapSettingsForm: View { .onChange(of: meshMapDistance) { _, newMeshMapDistance in UserDefaults.meshMapDistance = newMeshMapDistance } - Toggle(isOn: $waypoints) { - Label("Show Waypoints ", systemImage: "signpost.right.and.left") + Toggle(isOn: $enableMapWaypoints) { + Label { + Text("Show Waypoints") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.multicolor) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - UserDefaults.enableMapWaypoints = !waypoints + .tint(.accentColor) + } + Toggle(isOn: $enableMapShowFavorites) { + Label { + Text("Favorites") + } icon: { + Image(systemName: "star.fill") + .symbolRenderingMode(.multicolor) } } - + .tint(.accentColor) Toggle(isOn: $nodeHistory) { Label("Node History", systemImage: "building.columns.fill") } @@ -70,15 +81,10 @@ struct MapSettingsForm: View { self.nodeHistory.toggle() UserDefaults.enableMapNodeHistoryPins = self.nodeHistory } - Toggle(isOn: $routeLines) { + Toggle(isOn: $enableMapRouteLines) { Label("Route Lines", systemImage: "road.lanes") } - - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .onTapGesture { - self.routeLines.toggle() - UserDefaults.enableMapRouteLines = self.routeLines - } + .tint(.accentColor) Toggle(isOn: $convexHull) { Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right") } @@ -96,9 +102,14 @@ struct MapSettingsForm: View { UserDefaults.enableMapTraffic = self.traffic } Toggle(isOn: $pointsOfInterest) { - Label("Points of Interest", systemImage: "mappin.and.ellipse") + Label { + Text("Points of Interest") + } icon: { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.multicolor) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .tint(.accentColor) .onTapGesture { self.pointsOfInterest.toggle() UserDefaults.enableMapPointsOfInterest = self.pointsOfInterest diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index d3ef18a3..76e474b8 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -156,7 +156,7 @@ struct PositionPopover: View { if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) .font(idiom == .phone ? .callout : .body) } icon: { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index ad342cbc..2f74d51f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -14,6 +14,7 @@ import SwiftUI struct WaypointForm: View { @EnvironmentObject var bleManager: BLEManager + @Environment(\.managedObjectContext) var context @Environment(\.dismiss) private var dismiss @State var waypoint: WaypointEntity let distanceFormatter = MKDistanceFormatter() @@ -30,6 +31,7 @@ struct WaypointForm: View { @State private var lockedTo: Int64 = 0 @State private var detents: Set = [.medium, .fraction(0.85)] @State private var selectedDetent: PresentationDetent = .medium + @State private var waypointFailedAlert: Bool = false var body: some View { NavigationStack { @@ -47,7 +49,19 @@ struct WaypointForm: View { .textSelection(.enabled) .foregroundColor(.secondary) .font(.caption) + } + Button { + let currentLoc = LocationsHandler.currentLocation + waypoint.coordinate.longitude = currentLoc.longitude + waypoint.coordinate.latitude = currentLoc.latitude + } label: { + HStack { + Text("Use my Location") + Image(systemName: "location") + } + } + .accessibilityLabel("Set to current location") HStack { if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { DistanceText(meters: distance) @@ -72,6 +86,7 @@ struct WaypointForm: View { name = String(name.dropLast()) totalBytes = name.utf8.count } + waypoint.name = name.count > 0 ? name : "Dropped Pin" } } HStack { @@ -167,8 +182,8 @@ struct WaypointForm: View { if bleManager.sendWaypoint(waypoint: newWaypoint) { dismiss() } else { - dismiss() Logger.mesh.warning("Send waypoint failed") + waypointFailedAlert = true } } else { Logger.mesh.warning("Send waypoint failed, node not connected") @@ -196,11 +211,11 @@ struct WaypointForm: View { Menu { Button("For me", action: { - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() } dismiss() }) Button("For everyone", action: { @@ -225,16 +240,16 @@ struct WaypointForm: View { newWaypoint.expire = UInt32(1) if bleManager.sendWaypoint(waypoint: newWaypoint) { - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() } dismiss() } else { - dismiss() Logger.mesh.warning("Send waypoint failed") + waypointFailedAlert = true } }) } @@ -256,8 +271,8 @@ struct WaypointForm: View { Text(waypoint.name ?? "?") .font(.largeTitle) Spacer() - if waypoint.locked > 0 { - Image(systemName: "lock.fill" ) + if waypoint.locked > 0 && waypoint.locked != UInt32(BLEManager.shared.connectedPeripheral?.num ?? 0) { + Image(systemName: "lock.fill") .font(.largeTitle) } else { Button { @@ -368,21 +383,32 @@ struct WaypointForm: View { } } } + .alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) { + Button("OK", role: .cancel) { + context.delete(waypoint) + do { + try context.save() + } catch { + context.rollback() + } + dismiss() + } + } .onDisappear { if waypoint.id == 0 { // New, unsent waypoint created by the user: delete it - bleManager.context.delete(waypoint) + context.delete(waypoint) do { - try bleManager.context.save() + try context.save() } catch { - bleManager.context.rollback() + context.rollback() Logger.mesh.error("Failed to save context on waypoint deletion: \(error)") } } } .onAppear { if waypoint.id > 0 { - let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context) + let waypoint = getWaypoint(id: Int64(waypoint.id), context: context) name = waypoint.name ?? "Dropped Pin" description = waypoint.longDescription ?? "" icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 081e7adc..d98941bb 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,7 +46,8 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { + .accessibilityElement(children: .combine) + Section("Node") { // Node HStack(alignment: .center) { Spacer() CircleText( @@ -67,6 +68,7 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } + .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -74,6 +76,7 @@ struct NodeDetail: View { } Spacer() } + .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -86,6 +89,7 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } + .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -104,6 +108,7 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } + .accessibilityElement(children: .combine) HStack { Label { @@ -116,6 +121,32 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } + .accessibilityElement(children: .combine) + + if node.user?.keyMatch ?? false { + if let publicKey = node.user?.publicKey { + HStack { + Label { + Text("Public Key") + } icon: { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + Spacer() + Button(action: { + context.perform{ + UIPasteboard.general.string = publicKey.base64EncodedString() + } + }) { + HStack { + Image(systemName: "key.horizontal.fill") + Text("Copy") + } + } + } + .accessibilityElement(children: .combine) + } + } if let metadata = node.metadata { HStack { @@ -129,6 +160,7 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } + .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -142,6 +174,20 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } + .accessibilityElement(children: .combine) + } + if node.user?.unmessagable ?? false { + HStack { + Label { + Text("Messaging") + } icon: { + Image(systemName: "iphone.slash") + .symbolRenderingMode(.multicolor) + } + Spacer() + Text("Unmonitored") + } + .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -161,6 +207,7 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } + .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -179,7 +226,9 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -203,7 +252,9 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - }.onTapGesture { + } + .accessibilityElement(children: .combine) + .onTapGesture { dateFormatRelative.toggle() } } @@ -216,79 +267,84 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + // Group weather/environment data for better VoiceOver experience + VStack { + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.windSpeed { + let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond) + let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } + let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) + WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), + gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) + } + if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H { + let locale = NSLocale.current as NSLocale + let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) + let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) + RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel) + } + if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H { + let locale = NSLocale.current as NSLocale + let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) + let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) + RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel) + } + if let radiation = node.latestEnvironmentMetrics?.radiation { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature { + let locale = NSLocale.current as NSLocale + let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) + let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C" + SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit) + } + if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") } } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.windSpeed { - let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond) - let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } - let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) - WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), - gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) - } - if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H { - let locale = NSLocale.current as NSLocale - let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) - let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) - RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel) - } - if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H { - let locale = NSLocale.current as NSLocale - let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) - let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) - RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel) - } - if let radiation = node.latestEnvironmentMetrics?.radiation { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature { - let locale = NSLocale.current as NSLocale - let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) - let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C" - SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit) - } - if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } + // Apply accessibility properties to the environment section + .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -298,6 +354,7 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } + .accessibilityElement(children: .combine) } } Section("Logs") { @@ -454,12 +511,11 @@ struct NodeDetail: View { let connectedNode, self.bleManager.connectedPeripheral != nil { Section("Administration") { - if connectedNode.myInfo?.hasAdmin ?? false { + if UserDefaults.enableAdministration { Button { let adminMessageId = bleManager.requestDeviceMetadata( fromUser: connectedNode.user!, toUser: node.user!, - adminIndex: connectedNode.myInfo!.adminIndex, context: context ) if adminMessageId > 0 { @@ -486,8 +542,7 @@ struct NodeDetail: View { Button("Shutdown Node?", role: .destructive) { if !bleManager.sendShutdown( fromUser: connectedNode.user!, - toUser: node.user!, - adminIndex: connectedNode.myInfo!.adminIndex + toUser: node.user! ) { Logger.mesh.warning("Shutdown Failed") } @@ -509,8 +564,7 @@ struct NodeDetail: View { Button("Reboot node?", role: .destructive) { if !bleManager.sendReboot( fromUser: connectedNode.user!, - toUser: node.user!, - adminIndex: connectedNode.myInfo!.adminIndex + toUser: node.user! ) { Logger.mesh.warning("Reboot Failed") } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 07f3d92c..3c20a9e2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,6 +31,7 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } + .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -49,9 +50,11 @@ struct NodeInfoItem: View { .cornerRadius(5) } } + .accessibilityElement(children: .combine) } Spacer() } + .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -76,9 +79,10 @@ struct NodeInfoItem: View { if user.hwModel != "UNSET" { Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "Unset".localized))) } else { - Text(String("incomplete".localized)) + Text(String("Incomplete".localized)) } } + .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 063e073a..1a02cfd7 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -91,20 +91,21 @@ struct NodeListFilter: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) - Toggle(isOn: $isIgnored) { - - Label { - Text("Ignored") - } icon: { - - Image(systemName: "minus.circle.fill") - .symbolRenderingMode(.multicolor) - } - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) if filterTitle == "Node Filters" { + Toggle(isOn: $isIgnored) { + + Label { + Text("Ignored") + } icon: { + + Image(systemName: "minus.circle.fill") + .symbolRenderingMode(.multicolor) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + Toggle(isOn: $isEnvironment) { Label { Text("Environment") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 2978ceab..1ceb7583 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,9 +7,96 @@ import SwiftUI import CoreLocation +import Foundation struct NodeListItem: View { + // Accessibility: Synthesized description for VoiceOver + private var accessibilityDescription: String { + var desc = "" + if let shortName = node.user?.shortName { + // Format the shortName using the String extension method + desc = shortName.formatNodeNameForVoiceOver() + } else if let longName = node.user?.longName { + desc = longName + } else { + desc = "Unknown".localized + " " + "Node".localized + } + if connected { + desc += ", currently connected" + } + if node.favorite { + desc += ", favorite" + } + if node.lastHeard != nil { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) + desc += ", last heard " + relative + } + if node.isOnline { + desc += ", online" + } else { + desc += ", offline" + } + let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) + if let roleName = role?.name { + desc += ", role: \(roleName)" + } + if node.hopsAway > 0 { + desc += ", \(node.hopsAway) hops away" + } + if let battery = node.latestDeviceMetrics?.batteryLevel { + // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge + if battery > 100 { + desc += ", " + "Plugged in".localized + } else if battery == 100 { + desc += ", " + "Charging".localized + } else { + desc += ", battery \(battery)%" + } + } + // Add distance and heading/bearing if available, but only for non-connected nodes + if !connected, let (lastPosition, myCoord) = locationData { + let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + // Distance information + let distanceFormatter = LengthFormatter() + distanceFormatter.unitStyle = .medium + let formattedDistance = distanceFormatter.string(fromMeters: metersAway) + // For VoiceOver, prepend 'Distance' (localized) + desc += ", " + String(format: "%@: %@", "Distance".localized, formattedDistance) + // Add bearing/heading information for VoiceOver + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) + // Using a direct format without requiring a new localization key + desc += ", " + "Heading".localized + " " + formattedHeading + } + // Add signal strength if available + if node.snr != 0 && !node.viaMqtt { + let signalStrength: BLESignalStrength + if node.snr < -10 { + signalStrength = .weak + } else if node.snr < 5 { + signalStrength = .normal + } else { + signalStrength = .strong + } + let signalString: String + switch signalStrength { + case .weak: + signalString = "Signal strength weak".localized + case .normal: + signalString = "Signal strength normal".localized + case .strong: + signalString = "Signal strength strong".localized + } + desc += ", " + signalString + } + return desc + } + @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -85,6 +172,11 @@ struct NodeListItem: View { let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) IconAndText(systemName: role?.systemName ?? "figure", text: "Role: \(role?.name ?? "Unknown".localized)") + if node.user?.unmessagable ?? false { + IconAndText(systemName: "iphone.slash", + renderingMode: .multicolor, + text: "Unmonitored") + } if node.isStoreForwardRouter { IconAndText(systemName: "envelope.arrow.triangle.branch", renderingMode: .multicolor, @@ -167,7 +259,10 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - } + // Accessibility: Make the whole row a single element for VoiceOver + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityDescription) + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift new file mode 100644 index 00000000..4bc56e24 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -0,0 +1,92 @@ +// ShareContactQRDialog.swift +// Meshtastic +// +// Created by GitHub Copilot on 5/13/25. + +import SwiftUI +import CoreImage.CIFilterBuiltins +#if canImport(UIKit) +import UIKit +#endif +import CoreData +import MeshtasticProtobufs +import OSLog + +struct ShareContactQRDialog: View { + let node: NodeInfo + @Environment(\.dismiss) private var dismiss + var qrString: String { + var contact = SharedContact() + contact.nodeNum = node.num + contact.user = node.user + do { + let contactString = try contact.serializedData().base64EncodedString() + return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url()) + } catch { + Logger.services.error("Error serializing contact: \(error)") + return "" + } + } + var qrImage: UIImage { + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() + filter.setValue(Data(qrString.utf8), forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: 10, y: 10) + if let outputImage = filter.outputImage?.transformed(by: transform), + let cgimg = context.createCGImage(outputImage, from: outputImage.extent) { + return UIImage(cgImage: cgimg) + } + return UIImage(systemName: "xmark.circle") ?? UIImage() + } + var body: some View { + VStack(spacing: 20) { + Text("Share Contact QR") + .font(.title2) + .padding(.top) + Text(node.user.longName) + .font(.headline) + Image(uiImage: qrImage) + .interpolation(.none) + .resizable() + .scaledToFit() + .background(Color(.systemBackground)) + .cornerRadius(16) + .shadow(radius: 4) + Text("Scan this QR code to add \(node.user.longName) to another device.") + .font(.subheadline) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + ShareLink("Share QR Code & Link", + item: Image(uiImage: qrImage), + subject: Text("Add Meshtastic Node \(node.user.shortName) as a contact"), + message: Text(qrString), + preview: SharePreview("Add Meshtastic Node \(node.user.shortName) as a contact", + image: Image(uiImage: qrImage)) + ) + Button("Done") { dismiss() } + .buttonStyle(.borderedProminent) + .padding(.bottom) + } + .padding() + .frame(maxWidth: 350) + } +} + +#if DEBUG +struct ShareContactQRDialog_Previews: PreviewProvider { + static var previews: some View { + var node = NodeInfo() + node.num = 123456 + var userProto = User() + userProto.id = "!1234" + userProto.longName = "Bud" + userProto.shortName = "Bud" + userProto.hwModel = HardwareModel.tbeam + userProto.role = Config.DeviceConfig.Role.client + userProto.publicKey = Data() + node.user = userProto + + return ShareContactQRDialog(node: node) + } +} +#endif diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1e823020..21fd6fa5 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -28,6 +28,8 @@ struct NodeList: View { @State private var isFavorite = false @State private var isIgnored = false @State private var isEnvironment = false + // Force refresh ID to make SwiftUI rebuild the view hierarchy + @State private var forceRefreshID = UUID() @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @State private var hopsAway: Double = -1.0 @@ -38,6 +40,8 @@ struct NodeList: View { @State private var isPresentingPositionFailedAlert = false @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 + @State private var isPresentingShareContactQR = false + @State private var shareContactNode: NodeInfoEntity? var boolFilters: [Bool] {[ isFavorite, @@ -76,13 +80,21 @@ struct NodeList: View { /// Allow users to mute notifications for a node even if they are not connected if let user = node.user { NodeAlertsButton(context: context, node: node, user: user) + if !user.unmessagable { + Button(action: { + shareContactNode = node + isPresentingShareContactQR = true + }) { + Label("Share Contact QR", systemImage: "qrcode") + } + } } if let connectedNode { /// Favoriting a node requires being connected FavoriteNodeButton(bleManager: bleManager, context: context, node: node) /// Don't show message, trace route, position exchange or delete context menu items for the connected node if connectedNode.num != node.num { - if !node.viaMqtt || node.viaMqtt && node.hopsAway == 0 { + if !(node.user?.unmessagable ?? true) { Button(action: { if let url = URL(string: "meshtastic:///messages?userNum=\(node.num)") { UIApplication.shared.open(url) @@ -91,21 +103,10 @@ struct NodeList: View { Label("Message", systemImage: "message") } } - Button { - let traceRouteSent = bleManager.sendTraceRouteRequest( - destNum: node.num, - wantResponse: true - ) - if traceRouteSent { - isPresentingTraceRouteSentAlert = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - isPresentingTraceRouteSentAlert = false - } - } - - } label: { - Label("Trace Route", systemImage: "signpost.right.and.left") - } + TraceRouteButton( + bleManager: bleManager, + node: node + ) Button { let positionSent = bleManager.sendPosition( channel: node.channel, @@ -142,6 +143,7 @@ struct NodeList: View { } var body: some View { + // Use forceRefreshID to completely rebuild the view when notifications update the selected node NavigationSplitView(columnVisibility: $columnVisibility) { List(nodes, id: \.self, selection: $selectedNode) { node in NodeListItem( @@ -231,6 +233,13 @@ struct NodeList: View { } } } + } + .sheet(isPresented: $isPresentingShareContactQR) { + if let node = shareContactNode { + ShareContactQRDialog(node: node.toProto()) + } else { + EmptyView() + } } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems( @@ -243,6 +252,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -261,6 +272,7 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } + .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -269,6 +281,8 @@ struct NodeList: View { phoneOnly: true ) } + // Make sure the ZStack passes through accessibility to the ConnectedDevice component + .accessibilityElement(children: .contain) ) } } else { @@ -326,16 +340,40 @@ struct NodeList: View { } .onChange(of: router.navigationState) { if let selected = router.navigationState.nodeListSelectedNodeNum { - self.selectedNode = getNodeInfo(id: selected, context: context) + // Force a complete view rebuild by generating a new UUID + Logger.services.info("Forcing view rebuild with new ID: \(self.forceRefreshID)") + // First clear selection + self.forceRefreshID = UUID() + self.selectedNode = nil + // Then after a short delay, set the new selection. Makes it obvious to use page is refreshing too. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + // Generate another UUID to ensure view gets rebuilt + self.forceRefreshID = UUID() + self.selectedNode = getNodeInfo(id: selected, context: context) + Logger.services.info("Complete view refresh with node: \(selected, privacy: .public)") + } } else { self.selectedNode = nil } } .onAppear { + // Set up notification observer for forced refreshes from notifications + NotificationCenter.default.addObserver(forName: NSNotification.Name("ForceNavigationRefresh"), object: nil, queue: .main) { notification in + if let nodeNum = notification.userInfo?["nodeNum"] as? Int64 { + // Force complete refresh of view + self.forceRefreshID = UUID() + self.selectedNode = getNodeInfo(id: nodeNum, context: self.context) + Logger.services.info("NodeList directly updated from notification for node: \(nodeNum, privacy: .public)") + } + } Task { await searchNodeList() } } + .onDisappear { + // Remove observer when view disappears + NotificationCenter.default.removeObserver(self, name: NSNotification.Name("ForceNavigationRefresh"), object: nil) + } } private func searchNodeList() async { diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index e65a4c64..98355864 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -38,7 +38,9 @@ struct AboutMeshtastic: View { } } } - Link("Help with App Development", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!) + Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!) + .font(.title2) + Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!) .font(.title2) Button("Review the app") { if let scene = UIApplication.shared.connectedScenes diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index e16fb31b..eeddf8d0 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -6,13 +6,17 @@ import MapKit import OSLog struct AppSettings: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @State var totalDownloadedTileSize = "" @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false + @State private var purgeStaleNodes: Bool = false + @AppStorage("purgeStaleNodeDays") private var purgeStaleNodeDays: Double = 0 @AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true @AppStorage("enableAdministration") private var enableAdministration: Bool = false + @AppStorage("usageDataAndCrashReporting") private var usageDataAndCrashReporting: Bool = true var body: some View { VStack { Form { @@ -30,6 +34,13 @@ struct AppSettings: View { Text("PKI based node administration, requires firmware version 2.5+") .foregroundStyle(.secondary) .font(.caption) + Toggle(isOn: $usageDataAndCrashReporting) { + Label("Usage and Crash Data", systemImage: "pencil.and.list.clipboard") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Provide anonymous usage statistics and crash reports.") + .foregroundStyle(.secondary) + .font(.caption) } Section(header: Text("environment")) { VStack(alignment: .leading) { @@ -40,6 +51,39 @@ struct AppSettings: View { } } Section(header: Text("App Data")) { + Toggle(isOn: $purgeStaleNodes ) { + Label { + Text("Clear Stale Nodes") + } icon: { + Image(systemName: "list.bullet.circle") + } + } + .onFirstAppear { + purgeStaleNodes = purgeStaleNodeDays > 0 + Logger.services.info("ℹ️ Purge Stale Nodes toggle initialized to \(purgeStaleNodes)") + } + .onChange(of: purgeStaleNodes) { _, newValue in + purgeStaleNodeDays = purgeStaleNodeDays > 0 ? purgeStaleNodeDays : 7 + purgeStaleNodeDays = newValue ? purgeStaleNodeDays : 0 + Logger.services.info("ℹ️ Purge Stale Nodes changed to \(purgeStaleNodeDays)") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + .listRowSeparator(purgeStaleNodes ? .hidden : .visible) + if purgeStaleNodes { + VStack(alignment: .leading) { + Text(String(localized: "After \(Int(purgeStaleNodeDays)) Days")) + Slider(value: $purgeStaleNodeDays, in: 1...180, step: 1) { + } minimumValueLabel: { + Text("1") + } maximumValueLabel: { + Text("180") + } + } + Text("Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } Button { isPresentingCoreDataResetConfirm = true } label: { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 0a24c8c5..8e38f27b 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -47,6 +47,7 @@ struct Channels: View { /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" + @State private var showingHelp = false @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), @@ -124,6 +125,7 @@ struct Channels: View { .brightness(0.1) VStack { HStack { + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) @@ -239,6 +241,7 @@ struct Channels: View { #endif } } + if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { @@ -279,6 +282,29 @@ struct Channels: View { .padding() } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Channels") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 92f1b8e2..acbb11b0 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -148,7 +148,7 @@ struct ChannelForm: View { .listRowSeparator(.visible) .onChange(of: preciseLocation) { _, pl in if pl == false { - positionPrecision = 14 + positionPrecision = 15 } } } @@ -157,11 +157,11 @@ struct ChannelForm: View { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 11...14, step: 1) { + Slider(value: $positionPrecision, in: 12...15, step: 1) { } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { Image(systemName: "plus") + } maximumValueLabel: { + Image(systemName: "minus") } Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "") .foregroundColor(.gray) @@ -228,7 +228,7 @@ struct ChannelForm: View { .onChange(of: positionsEnabled) { _, pe in if pe { if positionPrecision == 0 { - positionPrecision = 14 + positionPrecision = 15 } } else { positionPrecision = 0 diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 82b4f628..be6b9522 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -80,7 +80,7 @@ struct BluetoothConfig: View { bc.enabled = enabled bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin bc.fixedPin = UInt32(fixedPin) ?? 123456 - let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -111,12 +111,11 @@ struct BluetoothConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.bluetoothConfig == nil { Logger.mesh.info("⚙️ Empty or expired bluetooth config requesting via PKI admin") - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty bluetooth config") - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index d1af3a6a..cb4f7aee 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -11,7 +11,7 @@ struct ConfigHeader: View { var body: some View { if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { - Text("There has been no response to a request for device metadata over the admin channel for this node.") + Text("There has been no response to a request for device metadata via PKC admin for this node.") .font(.callout) .foregroundColor(.orange) @@ -19,7 +19,7 @@ struct ConfigHeader: View { // Let users know what is going on if they are using remote admin and don't have the config yet let expiration = node?.sessionExpiration ?? Date() if node?[keyPath: config] == nil || expiration < node?.sessionExpiration ?? Date() { - Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node.") + Text("\(title) config data was requested via PKC admin but no response has been returned from the remote node.") .font(.callout) .foregroundColor(.orange) } else { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 6539848a..834dcc0f 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -204,11 +204,11 @@ struct DeviceConfig: View { .controlSize(.regular) .padding(.trailing) .confirmationDialog( - "All device and app data will be deleted.", + "Factory reset will delete device and app data.", isPresented: $isPresentingFactoryResetConfirm, titleVisibility: .visible ) { - Button("Factory reset your device and app? ", role: .destructive) { + Button("Delete all config? ", role: .destructive) { if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { bleManager.disconnectPeripheral() @@ -218,6 +218,16 @@ struct DeviceConfig: View { Logger.mesh.error("Factory Reset Failed") } } + Button("Delete all config, keys and BLE bonds? ", role: .destructive) { + if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!, resetDevice: true) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + bleManager.disconnectPeripheral() + clearCoreDataDatabase(context: context, includeRoutes: false) + } + } else { + Logger.mesh.error("Factory Reset Failed") + } + } } } } @@ -235,7 +245,7 @@ struct DeviceConfig: View { dc.disableTripleClick = !tripleClickAsAdHocPing dc.tzdef = tzdef dc.ledHeartbeatDisabled = !ledHeartbeatEnabled - let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -268,13 +278,12 @@ struct DeviceConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.deviceConfig == nil { Logger.mesh.info("⚙️ Empty or expired device config requesting via PKI admin") - _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { if node.deviceConfig == nil { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty device config") - _ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index d6f14135..682bfd45 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -27,6 +27,7 @@ struct DisplayConfig: View { @State var oledType = 0 @State var displayMode = 0 @State var units = 0 + @State var use12HourClock = false var body: some View { Form { @@ -74,6 +75,11 @@ struct DisplayConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) + Toggle(isOn: $use12HourClock) { + Label("12 Hour Clock", systemImage: "clock") + Text("Sets the screen clock format to 12-hour.") + } + .tint(Color.accentColor) } Section(header: Text("Timing & Format")) { VStack(alignment: .leading) { @@ -141,8 +147,9 @@ struct DisplayConfig: View { dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue() dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() dc.units = Units(rawValue: units)!.protoEnumValue() + dc.use12HClock = use12HourClock - let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -174,12 +181,11 @@ struct DisplayConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.displayConfig == nil { Logger.mesh.info("⚙️ Empty or expired display config requesting via PKI admin") - _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty display config") - _ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } @@ -212,6 +218,9 @@ struct DisplayConfig: View { .onChange(of: units) { oldUnits, newUnits in if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true } } + .onChange(of: use12HourClock) { oldUse12HourClock, newUse12HourClock in + if oldUse12HourClock != newUse12HourClock && newUse12HourClock != node?.displayConfig?.use12HClock { hasChanges = true } + } } func setDisplayValues() { self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0) @@ -223,6 +232,7 @@ struct DisplayConfig: View { self.oledType = Int(node?.displayConfig?.oledType ?? 0) self.displayMode = Int(node?.displayConfig?.displayMode ?? 0) self.units = Int(node?.displayConfig?.units ?? 0) - self.hasChanges = false + self.use12HourClock = node?.displayConfig?.use12HClock ?? false + self.hasChanges = node?.displayConfig?.use12HClock ?? false } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 196f6ffb..3e8beb9a 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -218,7 +218,7 @@ struct LoRaConfig: View { if connectedNode?.num ?? -1 == node?.user?.num ?? 0 { UserDefaults.modemPreset = modemPreset } - let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -249,12 +249,13 @@ struct LoRaConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.loRaConfig == nil { Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin") - _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + if connectedNode.user != nil && node.user != nil { + _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!) + } } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty lora config") - _ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 24c0ada3..ad3e5e3c 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -66,7 +66,7 @@ struct AmbientLightingConfig: View { al.blue = UInt32(components.blue * 255) } - let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -96,12 +96,11 @@ struct AmbientLightingConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.ambientLightingConfig == nil { Logger.mesh.info("⚙️ Empty or expired ambient lighting module config requesting via PKI admin") - _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty ambient lighting module config") - _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 20a62136..28ec19ad 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -79,6 +79,7 @@ struct CannedMessagesConfig: View { totalBytes = messages.utf8.count } hasMessagesChanges = true + hasChanges = true } .foregroundColor(.gray) } @@ -201,7 +202,7 @@ struct CannedMessagesConfig: View { cmc.inputbrokerEventCw = InputEventChars(rawValue: inputbrokerEventCw)!.protoEnumValue() cmc.inputbrokerEventCcw = InputEventChars(rawValue: inputbrokerEventCcw)!.protoEnumValue() cmc.inputbrokerEventPress = InputEventChars(rawValue: inputbrokerEventPress)!.protoEnumValue() - let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -211,7 +212,7 @@ struct CannedMessagesConfig: View { } } if hasMessagesChanges { - let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -244,12 +245,11 @@ struct CannedMessagesConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.cannedMessageConfig == nil { Logger.mesh.info("⚙️ Empty or expired canned messages module config requesting via PKI admin") - _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty canned messages module config") - _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index e042ca77..9d4b61a4 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -172,7 +172,7 @@ struct DetectionSensorConfig: View { dsc.usePullup = self.usePullup dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs) dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs) - let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -202,12 +202,11 @@ struct DetectionSensorConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.detectionSensorConfig == nil { Logger.mesh.info("⚙️ Empty or expired detection sensor module config requesting via PKI admin") - _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty detection sensor module config") - _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index e9959b41..d0c0b13b 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -180,7 +180,7 @@ struct ExternalNotificationConfig: View { enc.outputMs = UInt32(outputMilliseconds) enc.usePwm = usePWM enc.useI2SAsBuzzer = useI2SAsBuzzer - let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -210,12 +210,11 @@ struct ExternalNotificationConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.externalNotificationConfig == nil { Logger.mesh.info("⚙️ Empty or expired external notificaiton module config requesting via PKI admin") - _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty external notificaiton module config") - _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 6967a2ab..1e9ed3da 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -112,7 +112,6 @@ struct MQTTConfig: View { Label("I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT.", systemImage: "hand.raised") .foregroundColor(.gray) .font(.callout) - } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } @@ -130,11 +129,11 @@ struct MQTTConfig: View { Text("To comply with privacy laws like CCPA and GDPR, we avoid sharing exact location data. Instead, we use anonymized or approximate (imprecise) location information to protect your privacy.") .foregroundColor(.gray) .font(.callout) - Slider(value: $mapPositionPrecision, in: 11...14, step: 1) { + Slider(value: $mapPositionPrecision, in: 12...15, step: 1) { } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { Image(systemName: "plus") + } maximumValueLabel: { + Image(systemName: "minus") } Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "") .foregroundColor(.gray) @@ -269,7 +268,7 @@ struct MQTTConfig: View { mqtt.mapReportingEnabled = self.mapReportingEnabled mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision) mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs) - let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -361,12 +360,11 @@ struct MQTTConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.mqttConfig == nil { Logger.mesh.info("⚙️ Empty or expired mqtt module config requesting via PKI admin") - _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty mqtt module config") - _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } @@ -430,8 +428,13 @@ struct MQTTConfig: View { self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false self.mqttConnected = bleManager.mqttProxyConnected self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false - self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) + if node?.mqttConfig?.mapPublishIntervalSecs ?? 0 < 3600 { + self.mapPublishIntervalSecs = 3600 + } else { + self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600) + } self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14) + self.mapReportingOptIn = UserDefaults.mapReportingOptIn if mapPositionPrecision < 11 || mapPositionPrecision > 14 { self.mapPositionPrecision = 14 self.hasChanges = true diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 6d71f7a7..b0101f2b 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -69,12 +69,11 @@ struct PaxCounterConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.paxCounterConfig == nil { Logger.mesh.info("⚙️ Empty or expired pax counter module config requesting via PKI admin") - _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty pax counter module config") - _ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } @@ -101,8 +100,7 @@ struct PaxCounterConfig: View { let adminMessageId = bleManager.savePaxcounterModuleConfig( config: config, fromUser: fromUser, - toUser: toUser, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + toUser: toUser ) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index cc323170..a9b07c53 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -62,7 +62,7 @@ struct RangeTestConfig: View { rtc.enabled = enabled rtc.save = save rtc.sender = UInt32(sender) - let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -92,12 +92,11 @@ struct RangeTestConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.rangeTestConfig == nil { Logger.mesh.info("⚙️ Empty or expired range test module config requesting via PKI admin") - _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty range test module config") - _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 71452615..ab1663a4 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -53,7 +53,7 @@ struct RtttlConfig: View { SaveConfigButton(node: node, hasChanges: $hasChanges) { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if connectedNode != nil { - let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -83,12 +83,11 @@ struct RtttlConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.rtttlConfig == nil { Logger.mesh.info("⚙️ Empty or expired ringtone module config requesting via PKI admin") - _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty ringtone module config") - _ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 0bbe9fbc..daa4c21e 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -116,7 +116,7 @@ struct SerialConfig: View { sc.overrideConsoleSerialPort = overrideConsoleSerialPort sc.mode = SerialModeTypes(rawValue: mode)!.protoEnumValue() - let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -147,12 +147,11 @@ struct SerialConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.serialConfig == nil { Logger.mesh.info("⚙️ Empty or expired serial module config requesting via PKI admin") - _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty serial module config") - _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index e9fc429f..6f30ee33 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -118,7 +118,7 @@ struct StoreForwardConfig: View { sfc.records = UInt32(self.records) sfc.historyReturnMax = UInt32(self.historyReturnMax) sfc.historyReturnWindow = UInt32(self.historyReturnWindow) - let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -148,12 +148,11 @@ struct StoreForwardConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.storeForwardConfig == nil { Logger.mesh.info("⚙️ Empty or expired store & forward module config requesting via PKI admin") - _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty store & forward module config") - _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 0dfe7566..f87e7890 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -115,7 +115,7 @@ struct TelemetryConfig: View { tc.powerMeasurementEnabled = powerMeasurementEnabled tc.powerUpdateInterval = UInt32(powerUpdateInterval) tc.powerScreenEnabled = powerScreenEnabled - let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -145,12 +145,11 @@ struct TelemetryConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.telemetryConfig == nil { Logger.mesh.info("⚙️ Empty or expired telemetry module config requesting via PKI admin") - _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty telemetry module config") - _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 35e25660..9d92ca03 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -37,7 +37,7 @@ struct NetworkConfig: View { Toggle(isOn: $wifiEnabled) { Label("Enabled", systemImage: "wifi") - Text("Enabling WiFi will disable the bluetooth connection to the app.") + Text("Enabling WiFi will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -83,9 +83,9 @@ struct NetworkConfig: View { Section(header: Text("Ethernet Options")) { Toggle(isOn: $ethEnabled) { Label("Enabled", systemImage: "network") - Text("Enabling Ethernet will disable the bluetooth connection to the app.") + Text("Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.") } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .tint(.accentColor) } } @@ -95,6 +95,7 @@ struct NetworkConfig: View { Label("Enabled", systemImage: "point.3.connected.trianglepath.dotted") Text("Enable broadcasting packets via UDP over the local network.") } + .tint(.accentColor) } } } @@ -113,7 +114,7 @@ struct NetworkConfig: View { network.enabledProtocols = self.udpEnabled ? UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue) : UInt32(Config.NetworkConfig.ProtocolFlags.noBroadcast.rawValue) // network.addressMode = Config.NetworkConfig.AddressMode.dhcp - let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -139,7 +140,7 @@ struct NetworkConfig: View { Logger.mesh.info("empty network config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { - _ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + _ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!) } } } @@ -154,12 +155,11 @@ struct NetworkConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.networkConfig == nil { Logger.mesh.info("⚙️ Empty or expired network config requesting via PKI admin") - _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty network config") - _ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 8fbd9d14..537c1f26 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -345,7 +345,7 @@ struct PositionConfig: View { if includeSpeed { pf.insert(.Speed) } if includeHeading { pf.insert(.Heading) } pc.positionFlags = UInt32(pf.rawValue) - let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!) if adminMessageId > 0 { // Disable the button after a successful save hasChanges = false @@ -412,12 +412,11 @@ struct PositionConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.positionConfig == nil { Logger.mesh.info("⚙️ Empty or expired position config requesting via PKI admin") - _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty position config") - _ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index e3c26ffb..6087fec6 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -139,12 +139,11 @@ struct PowerConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.powerConfig == nil { Logger.mesh.info("⚙️ Empty or expired power config requesting via PKI admin") - _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty power config") - _ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } @@ -195,8 +194,7 @@ struct PowerConfig: View { let adminMessageId = bleManager.savePowerConfig( config: config, fromUser: fromUser, - toUser: toUser, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + toUser: toUser ) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 938a9202..0153bb49 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -10,6 +10,7 @@ import SwiftUI import CoreData import MeshtasticProtobufs import OSLog +import CryptoKit struct SecurityConfig: View { @@ -33,7 +34,18 @@ struct SecurityConfig: View { @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false - @State var adminChannelEnabled = false + @State var privateKeyIsSecure = true + @State var backupStatus: KeyBackupStatus? + @State var backupStatusError: OSStatus? + + private var isValidKeyPair: Bool { + guard let privateKeyBytes = Data(base64Encoded: privateKey), + let calculatedPublicKey = generatePublicKeyDisplay(from: privateKeyBytes), + let decodedPublicKey = Data(base64Encoded: publicKey) else { + return false + } + return calculatedPublicKey == decodedPublicKey + } var body: some View { VStack { @@ -41,7 +53,7 @@ struct SecurityConfig: View { ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues) Text("Security Config Settings require a firmware version 2.5+") .font(.title3) - Section(header: Text("Admin & Direct Message Keys")) { + Section(header: Text("Direct Message Key")) { VStack(alignment: .leading) { Label("Public Key", systemImage: "key") Text(publicKey) @@ -52,12 +64,16 @@ struct SecurityConfig: View { .foregroundStyle(.tertiary) .disableAutocorrection(true) .textSelection(.enabled) - Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(isValidKeyPair ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) Divider() Label("Private Key", systemImage: "key.fill") - SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) + SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey, isSecure: $privateKeyIsSecure) .background( RoundedRectangle(cornerRadius: 10.0) .stroke(hasValidPrivateKey ? Color.clear : Color.red, lineWidth: 2.0) @@ -65,38 +81,123 @@ struct SecurityConfig: View { Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) + if let currentNode = node { + Divider() + Label("Key Backup", systemImage: "icloud") + HStack(alignment: .firstTextBaseline) { + let keychainKey = "PrivateKeyNode\(currentNode.num)" + Button { + let status = KeychainHelper.standard.save(key: keychainKey, value: privateKey) + if status == errSecSuccess { + backupStatus = KeyBackupStatus.saved + } else { + backupStatus = KeyBackupStatus.saveFailed + backupStatusError = status + } + } + label: { + Image(systemName: "icloud.and.arrow.up") + Text("Backup") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + Spacer() + Button { + if let value = KeychainHelper.standard.read(key: keychainKey) { + self.privateKey = value + self.privateKeyIsSecure = false + backupStatus = KeyBackupStatus.restored + } else { + backupStatus = KeyBackupStatus.restoreFailed + } + } + label: { + Image(systemName: "key.icloud") + Text("Restore") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + Spacer() + Button { + let status = KeychainHelper.standard.delete(key: keychainKey) + if status == errSecSuccess { + backupStatus = KeyBackupStatus.deleted + } else { + backupStatus = KeyBackupStatus.deleteFailed + } + } + label: { + Image(systemName: "trash") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + if let status = backupStatus { + let state = status.success + Text("\(status.description)") + .font(.caption) + .foregroundColor(state ? .green : .red) + } + Text("Backup your private key to your iCloud keychain.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } Divider() - Label("Primary Admin Key", systemImage: "key.viewfinder") - SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0) - ) - Text("The primary public key authorized to send admin messages to this node.") - .foregroundStyle(.secondary) - .font(idiom == .phone ? .caption : .callout) - Divider() - Label("Secondary Admin Key", systemImage: "key.viewfinder") - SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0) - ) - Text("The secondary public key authorized to send admin messages to this node.") - .foregroundStyle(.secondary) - .font(idiom == .phone ? .caption : .callout) - Divider() - Label("Tertiary Admin Key", systemImage: "key.viewfinder") - SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey3) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) - ) - Text("The tertiary public key authorized to send admin messages to this node.") + HStack(alignment: .firstTextBaseline) { + Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle") + Spacer() + Button { + if let keyBytes = generatePrivateKey(count: 32) { + privateKey = keyBytes.base64EncodedString() + self.privateKeyIsSecure = false + } + } label: { + Image(systemName: "lock.rotation") + .font(.title) + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + Text("Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } } + Section(header: Text("Admin Keys")) { + Label("Primary Admin Key", systemImage: "key.viewfinder") + SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The primary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Secondary Admin Key", systemImage: "key.viewfinder") + SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The secondary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Tertiary Admin Key", systemImage: "key.viewfinder") + SecureInput("Tertiary Admin Key", text: $adminKey3, isValid: $hasValidAdminKey3) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The tertiary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } Section(header: Text("Logs")) { Toggle(isOn: $serialEnabled) { Label("Serial Console", systemImage: "terminal") @@ -109,19 +210,14 @@ struct SecurityConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Section(header: Text("Administration")) { - if adminKey.length > 0 || adminChannelEnabled { + if adminKey.length > 0 || UserDefaults.enableAdministration { + Section(header: Text("Administration")) { Toggle(isOn: $isManaged) { Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") Text("Device is managed by a mesh administrator, the user is unable to access any of the device settings.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Toggle(isOn: $adminChannelEnabled) { - Label("Legacy Administration", systemImage: "lock.slash") - Text("Allow incoming device control over the insecure legacy admin channel.") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } } @@ -134,6 +230,9 @@ struct SecurityConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) + .onChange(of: node) { _, newNode in + setSecurityValues() + } .onChange(of: isManaged) { _, newIsManaged in if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true } } @@ -143,17 +242,18 @@ struct SecurityConfig: View { .onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } } - .onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in - if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true } - } - .onChange(of: privateKey) { + .onChange(of: privateKey) { _, key in let tempKey = Data(base64Encoded: privateKey) ?? Data() if tempKey.count == 32 { hasValidPrivateKey = true + if let privateKeyBytes = Data(base64Encoded: privateKey), privateKeyBytes.count == 32 { + // Valid private key -- generate the public key + publicKey = generatePublicKeyDisplay(from: privateKeyBytes)?.base64EncodedString() ?? "" + } } else { hasValidPrivateKey = false } - hasChanges = true + if key != node?.securityConfig?.privateKey?.base64EncodedString() ?? "" && hasValidPrivateKey { hasChanges = true } } .onChange(of: adminKey) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() @@ -164,7 +264,7 @@ struct SecurityConfig: View { } else { hasValidAdminKey = false } - hasChanges = true + if key != node?.securityConfig?.adminKey?.base64EncodedString() ?? "" && hasValidAdminKey { hasChanges = true } } .onChange(of: adminKey2) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() @@ -175,7 +275,7 @@ struct SecurityConfig: View { } else { hasValidAdminKey2 = false } - hasChanges = true + if key != node?.securityConfig?.adminKey2?.base64EncodedString() ?? "" && hasValidAdminKey2 { hasChanges = true } } .onChange(of: adminKey3) { _, key in let tempKey = Data(base64Encoded: key) ?? Data() @@ -186,10 +286,10 @@ struct SecurityConfig: View { } else { hasValidAdminKey3 = false } - hasChanges = true + if key != node?.securityConfig?.adminKey3?.base64EncodedString() ?? "" && hasValidAdminKey3 { hasChanges = true } } .onFirstAppear { - // Need to request a DeviceConfig from the remote node before allowing changes + // Need to request a SecurityConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) if let connectedNode { @@ -199,13 +299,12 @@ struct SecurityConfig: View { let expiration = node.sessionExpiration ?? Date() if expiration < Date() || node.securityConfig == nil { Logger.mesh.info("⚙️ Empty or expired security config requesting via PKI admin") - _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!) } } else { if node.deviceConfig == nil { /// Legacy Administration - Logger.mesh.info("☠️ Using insecure legacy admin, empty security config") - _ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") } } } @@ -226,24 +325,41 @@ struct SecurityConfig: View { } var config = Config.SecurityConfig() - config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() config.adminKey = [Data(base64Encoded: adminKey) ?? Data(), Data(base64Encoded: adminKey2) ?? Data(), Data(base64Encoded: adminKey3) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled - config.adminChannelEnabled = adminChannelEnabled + let keyUpdated = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey let adminMessageId = bleManager.saveSecurityConfig( config: config, fromUser: fromUser, - toUser: toUser, - adminIndex: connectedNode.myInfo?.adminIndex ?? 0 + toUser: toUser ) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save + if keyUpdated { + node?.user?.publicKey = Data(base64Encoded: publicKey) ?? Data() + do { + try context.save() + Logger.data.info("💾 Saved UserEntity Public Key to Core Data for \(node?.num ?? 0, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data UserEntity: \(nsError, privacy: .public)") + } + } hasChanges = false + if keyUpdated { + if !bleManager.sendReboot( + fromUser: fromUser, + toUser: toUser + ) { + Logger.mesh.warning("Reboot Failed") + } + } goBack() } } @@ -258,7 +374,49 @@ struct SecurityConfig: View { self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false - self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false self.hasChanges = false } + + func generatePrivateKey(count: Int) -> Data? { + var randomBytes = Data(count: count) + let status = randomBytes.withUnsafeMutableBytes { (mutableBytes: UnsafeMutableRawBufferPointer) -> Int32 in + guard let pointer = mutableBytes.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return -1 // Indicate an error + } + return SecRandomCopyBytes(kSecRandomDefault, count, pointer) + } + + if status == errSecSuccess { + // Generate a random "f" value and then adjust the value to make + // it valid as an "s" value for eval(). According to the specification + // we need to mask off the 3 right-most bits of f[0], mask off the + // left-most bit of f[31], and set the second to left-most bit of f[31]. + var f = randomBytes + f[0] &= 0xF8 + f[31] = (f[31] & 0x7F) | 0x40 + return f + } else { + // Handle error, perhaps by logging or throwing an exception + Logger.mesh.debug("Error generating random bytes: \(status)") + return nil + } + } + + // Generate a new public key for display purposes to show the user what will be changed after the new private key is saved to the device + func generatePublicKeyDisplay(from privateKeyData: Data) -> Data? { + guard privateKeyData.count == 32 else { + Logger.mesh.debug("Invalid private key length. Must be 32 bytes for Curve25519.") + return nil + } + + do { + // Create a Curve25519 private key from raw representation + let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData) + let publicKey = privateKey.publicKey + return publicKey.rawRepresentation + } catch { + Logger.mesh.debug("Failed to create Curve25519 key: \(error)") + return nil + } + } } diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index ad885d86..f8225e73 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -160,7 +160,7 @@ struct Firmware: View { Button { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) if connectedNode != nil { - if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { + if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!) { Logger.mesh.error("Reboot Failed") } } @@ -196,6 +196,9 @@ struct Firmware: View { } } Api().loadFirmwareReleaseData { (fw) in + latestStable = fw.releases.stable.first + let archString = currentDevice?.architecture.rawValue ?? "" + let ls = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) latestStable = fw.releases.stable.first latestAlpha = fw.releases.alpha.first } diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 892df6eb..49c78b0c 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -5,16 +5,24 @@ // Copyright(c) Garth Vander Houwen 7/13/22. // import SwiftUI +import CoreData +import OSLog +import MeshtasticProtobufs struct SaveChannelQRCode: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.managedObjectContext) var context - var channelSetLink: String + let channelSetLink: String var addChannels: Bool = false var bleManager: BLEManager - @State var showError: Bool = false - @State var connectedToDevice = false + + @State private var showError: Bool = false + @State private var errorMessage: String = "" + @State private var connectedToDevice: Bool = false + @State private var loraChanges: [String] = [] + @State private var okToMQTT: Bool = false + var body: some View { VStack { @@ -26,20 +34,50 @@ struct SaveChannelQRCode: View { .font(.title3) .padding() + if !loraChanges.isEmpty { + VStack(alignment: .leading) { + Text("LoRa Config Changes:") + .font(.headline) + .padding(.bottom, 5) + ForEach(loraChanges, id: \.self) { change in + Text("• \(change)") + .font(.callout) + .foregroundColor(.orange) + } + } + .padding() + } + if showError { - Text("Channels being added from the QR code did not save. When adding channels the names must be unique.") + Text(errorMessage.isEmpty ? "Channels being added from the QR code did not save. When adding channels the names must be unique." : errorMessage) .fixedSize(horizontal: false, vertical: true) .foregroundColor(.red) .font(.callout) .padding() } + HStack { if !showError { Button { - let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels) + // Extract channel data if it's a full URL + let channelData: String + if channelSetLink.hasPrefix("http") || channelSetLink.hasPrefix("meshtastic://") { + guard let extractedData = extractChannelDataFromURL(channelSetLink) else { + Logger.data.error("Failed to extract channel data from URL during save: \(channelSetLink)") + errorMessage = "Invalid channel URL format" + showError = true + return + } + channelData = extractedData + } else { + channelData = channelSetLink + } + + let success = bleManager.saveChannelSet(base64UrlString: channelData, addChannels: addChannels, okToMQTT: okToMQTT) if success { dismiss() } else { + errorMessage = "Failed to save channel configuration" showError = true } } label: { @@ -50,24 +88,23 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() .disabled(!connectedToDevice) -#if targetEnvironment(macCatalyst) - Button { - dismiss() - } label: { - Label("Cancel", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() -#endif + #if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("Cancel", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + #endif } else { Button { dismiss() } label: { Label("Cancel", systemImage: "xmark") - } .buttonStyle(.bordered) .buttonBorderShape(.capsule) @@ -77,7 +114,226 @@ struct SaveChannelQRCode: View { } } .onAppear { + Logger.data.info("Ch set link \(channelSetLink)") connectedToDevice = bleManager.connectToPreferredPeripheral() + fetchLoRaConfigChanges() } } + + private func extractChannelDataFromURL(_ urlString: String) -> String? { + Logger.data.info("Extracting channel data from URL: \(urlString)") + + + if let url = URL(string: urlString) { + // Get the fragment (part after #) + if let fragment = url.fragment, !fragment.isEmpty { + Logger.data.info("Extracted fragment from URL: \(fragment)") + return fragment + } + } + + // Fallback: manually extract everything after the last # + if let hashIndex = urlString.lastIndex(of: "#") { + let startIndex = urlString.index(after: hashIndex) + let channelData = String(urlString[startIndex...]) + if !channelData.isEmpty { + Logger.data.info("Extracted channel data manually: \(channelData)") + return channelData + } + } + + Logger.data.error("Failed to extract channel data from URL: \(urlString)") + return nil + } + + private func fetchLoRaConfigChanges() { + var currentLoRaConfig: Config.LoRaConfig? + + // First, extract the actual channel data from the URL if it's a full URL + let channelData: String + if channelSetLink.hasPrefix("http") || channelSetLink.hasPrefix("meshtastic://") { + guard let extractedData = extractChannelDataFromURL(channelSetLink) else { + Logger.data.error("Failed to extract channel data from URL: \(channelSetLink)") + errorMessage = "Invalid channel URL format" + showError = true + return + } + channelData = extractedData + } else { + // Assume it's already the base64 data + channelData = channelSetLink + } + + Logger.data.info("Processing channel data: \(channelData)") + + // Fetch current LoRa config from Core Data + let fetchRequest = NodeInfoEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? 0)) + + do { + let nodes = try context.fetch(fetchRequest) + if let node = nodes.first { + currentLoRaConfig = node.loRaConfig?.toProto() + } + } catch { + Logger.data.error("Failed to fetch NodeInfoEntity: \(error.localizedDescription, privacy: .public)") + } + + // Decode base64url string + let decodedString = channelData.base64urlToBase64() + guard let decodedData = Data(base64Encoded: decodedString) else { + Logger.data.error("Invalid base64 for ChannelSet data: \(channelData, privacy: .public)") + errorMessage = "Invalid channel data format" + showError = true + return + } + + do { + let channelSet = try ChannelSet(serializedBytes: decodedData) + let newLoRaConfig = channelSet.loraConfig + var changes: [String] = [] + + // Preserve user's current okToMQTT setting + okToMQTT = currentLoRaConfig?.configOkToMqtt ?? false + + if let current = currentLoRaConfig { + // Compare each field and track changes + if current.hopLimit != newLoRaConfig.hopLimit { + changes.append("Hop Limit: \(current.hopLimit) -> \(newLoRaConfig.hopLimit)") + } + if current.region != newLoRaConfig.region { + let currentRegionDesc = RegionCodes(rawValue: Int(current.region.rawValue))?.description ?? "Unknown" + let newRegionDesc = RegionCodes(rawValue: Int(newLoRaConfig.region.rawValue))?.description ?? "Unknown" + changes.append("Region: \(currentRegionDesc) -> \(newRegionDesc)") + } + if current.modemPreset != newLoRaConfig.modemPreset { + let currentPresetDesc = ModemPresets(rawValue: Int(current.modemPreset.rawValue))?.description ?? "Unknown" + let newPresetDesc = ModemPresets(rawValue: Int(newLoRaConfig.modemPreset.rawValue))?.description ?? "Unknown" + changes.append("Modem Preset: \(currentPresetDesc) -> \(newPresetDesc)") + } + if current.usePreset != newLoRaConfig.usePreset { + changes.append("Use Preset: \(current.usePreset) -> \(newLoRaConfig.usePreset)") + } + if current.txEnabled != newLoRaConfig.txEnabled { + changes.append("Transmit Enabled: \(current.txEnabled) -> \(newLoRaConfig.txEnabled)") + } + if current.txPower != newLoRaConfig.txPower { + changes.append("Transmit Power: \(current.txPower)dBm -> \(newLoRaConfig.txPower)dBm") + } + if current.channelNum != newLoRaConfig.channelNum { + changes.append("Channel Number: \(current.channelNum) -> \(newLoRaConfig.channelNum)") + } + if current.bandwidth != newLoRaConfig.bandwidth { + changes.append("Bandwidth: \(current.bandwidth) -> \(newLoRaConfig.bandwidth)") + } + if current.codingRate != newLoRaConfig.codingRate { + changes.append("Coding Rate: \(current.codingRate) -> \(newLoRaConfig.codingRate)") + } + if current.spreadFactor != newLoRaConfig.spreadFactor { + changes.append("Spread Factor: \(current.spreadFactor) -> \(newLoRaConfig.spreadFactor)") + } + if current.sx126XRxBoostedGain != newLoRaConfig.sx126XRxBoostedGain { + changes.append("RX Boosted Gain: \(current.sx126XRxBoostedGain) -> \(newLoRaConfig.sx126XRxBoostedGain)") + } + if current.overrideFrequency != newLoRaConfig.overrideFrequency { + changes.append("Override Frequency: \(current.overrideFrequency) -> \(newLoRaConfig.overrideFrequency)") + } + if current.ignoreMqtt != newLoRaConfig.ignoreMqtt { + changes.append("Ignore MQTT: \(current.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)") + } + } else { + // Compare against default values when no current config exists + let defaultConfig = getDefaultLoRaConfig() + + if newLoRaConfig.hopLimit != defaultConfig.hopLimit { + changes.append("Hop Limit: \(defaultConfig.hopLimit) -> \(newLoRaConfig.hopLimit)") + } + if newLoRaConfig.region != defaultConfig.region { + let newRegionDesc = RegionCodes(rawValue: Int(newLoRaConfig.region.rawValue))?.description ?? "Unknown" + changes.append("Region: Unset -> \(newRegionDesc)") + } + if newLoRaConfig.modemPreset != defaultConfig.modemPreset { + let newPresetDesc = ModemPresets(rawValue: Int(newLoRaConfig.modemPreset.rawValue))?.description ?? "Unknown" + changes.append("Modem Preset: Long Fast -> \(newPresetDesc)") + } + if newLoRaConfig.usePreset != defaultConfig.usePreset { + changes.append("Use Preset: \(defaultConfig.usePreset) -> \(newLoRaConfig.usePreset)") + } + if newLoRaConfig.txEnabled != defaultConfig.txEnabled { + changes.append("Transmit Enabled: \(defaultConfig.txEnabled) -> \(newLoRaConfig.txEnabled)") + } + if newLoRaConfig.txPower != defaultConfig.txPower { + changes.append("Transmit Power: \(defaultConfig.txPower)dBm -> \(newLoRaConfig.txPower)dBm") + } + if newLoRaConfig.channelNum != defaultConfig.channelNum { + changes.append("Channel Number: \(defaultConfig.channelNum) -> \(newLoRaConfig.channelNum)") + } + if newLoRaConfig.bandwidth != defaultConfig.bandwidth { + changes.append("Bandwidth: \(defaultConfig.bandwidth) -> \(newLoRaConfig.bandwidth)") + } + if newLoRaConfig.codingRate != defaultConfig.codingRate { + changes.append("Coding Rate: \(defaultConfig.codingRate) -> \(newLoRaConfig.codingRate)") + } + if newLoRaConfig.spreadFactor != defaultConfig.spreadFactor { + changes.append("Spread Factor: \(defaultConfig.spreadFactor) -> \(newLoRaConfig.spreadFactor)") + } + if newLoRaConfig.sx126XRxBoostedGain != defaultConfig.sx126XRxBoostedGain { + changes.append("RX Boosted Gain: \(defaultConfig.sx126XRxBoostedGain) -> \(newLoRaConfig.sx126XRxBoostedGain)") + } + if newLoRaConfig.overrideFrequency != defaultConfig.overrideFrequency { + changes.append("Override Frequency: \(defaultConfig.overrideFrequency) -> \(newLoRaConfig.overrideFrequency)") + } + if newLoRaConfig.ignoreMqtt != defaultConfig.ignoreMqtt { + changes.append("Ignore MQTT: \(defaultConfig.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)") + } + } + + loraChanges = changes + + } catch { + Logger.data.error("Failed to decode ChannelSet: \(error.localizedDescription, privacy: .public)") + errorMessage = "Failed to decode channel configuration" + showError = true + } + } + + private func getDefaultLoRaConfig() -> Config.LoRaConfig { + var config = Config.LoRaConfig() + config.hopLimit = 3 + config.region = .unset + config.modemPreset = .longFast + config.usePreset = true + config.txEnabled = true + config.txPower = 0 + config.channelNum = 0 + config.bandwidth = 0 + config.codingRate = 0 + config.spreadFactor = 0 + config.sx126XRxBoostedGain = false + config.overrideFrequency = 0.0 + config.ignoreMqtt = false + config.configOkToMqtt = false + return config + } +} + +extension LoRaConfigEntity { + func toProto() -> Config.LoRaConfig { + var config = Config.LoRaConfig() + config.hopLimit = UInt32(self.hopLimit) + config.region = Config.LoRaConfig.RegionCode(rawValue: Int(self.regionCode)) ?? .unset + config.modemPreset = Config.LoRaConfig.ModemPreset(rawValue: Int(self.modemPreset)) ?? .longFast + config.usePreset = self.usePreset + config.txEnabled = self.txEnabled + config.txPower = Int32(self.txPower) + config.channelNum = UInt32(self.channelNum) + config.bandwidth = UInt32(self.bandwidth) + config.codingRate = UInt32(self.codingRate) + config.spreadFactor = UInt32(self.spreadFactor) + config.sx126XRxBoostedGain = self.sx126xRxBoostedGain + config.overrideFrequency = self.overrideFrequency + config.ignoreMqtt = self.ignoreMqtt + config.configOkToMqtt = self.okToMqtt + return config + } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 2dec1530..426d95e9 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -432,7 +432,7 @@ struct Settings: View { let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil {// && node?.metadata == nil { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, context: context) if adminMessageId > 0 { Logger.mesh.info("Sent node metadata request from node details") } @@ -440,6 +440,7 @@ struct Settings: View { } } TipView(AdminChannelTip(), arrowEdge: .top) + .tipViewStyle(PersistentTip()) } else { if bleManager.connectedPeripheral != nil { Text("Connected Node \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index fa0e6370..a3788ff0 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -49,6 +49,7 @@ struct ShareChannels: View { var node: NodeInfoEntity? @State private var channelsUrl = "https://www.meshtastic.org/e/#" var qrCodeImage = QrCodeImage() + @State private var showingHelp = false var body: some View { @@ -82,13 +83,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()) - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) } else if channel.index == 1 && channel.role > 0 { Toggle("Channel 1 Included", isOn: $includeChannel1) .toggleStyle(.switch) @@ -96,7 +91,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -109,7 +104,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -122,7 +117,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -135,7 +130,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -148,7 +143,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -161,7 +156,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -174,7 +169,7 @@ struct ShareChannels: View { .disabled(channel.role == 1) Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") + Image(systemName: "lock.slash.fill") .foregroundColor(.red) } else { Image(systemName: "lock.fill") @@ -216,16 +211,39 @@ struct ShareChannels: View { .resizable() .scaledToFit() .frame( - minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), + minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), alignment: .top ) } } } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Generate QR Code") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: @@ -313,7 +331,7 @@ struct ShareChannels: View { guard let settingsString = try? channelSet.serializedData().base64EncodedString() else { return } - channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#" + settingsString.base64ToBase64url()) + channelsUrl = ("https://meshtastic.org/e/\(replaceChannels ? "" : "?add=true")#\(settingsString.base64ToBase64url())") } } } diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index 4a47255c..d281dc86 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -25,13 +25,14 @@ struct UserConfig: View { @State var hasChanges = false @State var shortName = "" @State var longName: String = "" + @State var isUnmessagable: Bool = false @State var isLicensed = false @State var overrideDutyCycle = false @State var overrideFrequency: Float = 0.0 @State var txPower = 0 - @FocusState var focusedField: Field? + public var minimumVersion = "2.6.9" let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -96,6 +97,14 @@ struct UserConfig: View { Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.") .foregroundColor(.gray) .font(.callout) + let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || self.minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame + Toggle(isOn: $isUnmessagable) { + Label("Unmessagable", systemImage: "iphone.slash") + Text("Used to identify unmonitored or infrastructure nodes so that messaging is not avaliable to nodes that will never respond.") + .font(.caption2) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .disabled(!supportedVersion) } // Only manage ham mode for the locally connected node if node?.num ?? 0 > 0 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { @@ -166,7 +175,8 @@ struct UserConfig: View { var u = User() u.shortName = shortName u.longName = longName - let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + u.isUnmessagable = isUnmessagable + let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!) if adminMessageId > 0 { hasChanges = false goBack() @@ -174,10 +184,11 @@ struct UserConfig: View { } else { var ham = HamParameters() ham.shortName = shortName + // ham.isUnmessagable = isUnmessagable ham.callSign = longName ham.txPower = Int32(txPower) ham.frequency = overrideFrequency - let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!) if adminMessageId > 0 { hasChanges = false goBack() @@ -199,6 +210,7 @@ struct UserConfig: View { .onAppear { self.shortName = node?.user?.shortName ?? "" self.longName = node?.user?.longName ?? "" + self.isUnmessagable = node?.user?.unmessagable ?? false self.isLicensed = node?.user?.isLicensed ?? false self.txPower = Int(node?.loRaConfig?.txPower ?? 0) self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.00 diff --git a/MeshtasticProtobufs/Package.resolved b/MeshtasticProtobufs/Package.resolved new file mode 100644 index 00000000..a679a95e --- /dev/null +++ b/MeshtasticProtobufs/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "a2385deee281bd55bce80722a1f2b020f7b745c02005befa8ccbf58a39ef4002", + "pins" : [ + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", + "version" : "1.29.0" + } + } + ], + "version" : 3 +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index e450d566..2b539ef6 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/admin.proto @@ -24,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. /// This message is used to do settings operations to both remote AND local nodes. /// (Prior to 1.2 these operations were done via special ToRadio operations) -public struct AdminMessage { +public struct AdminMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -291,6 +292,17 @@ public struct AdminMessage { set {payloadVariant = .removeBackupPreferences(newValue)} } + /// + /// Send an input event to the node. + /// This is used to trigger physical input events like button presses, touch events, etc. + public var sendInputEvent: AdminMessage.InputEvent { + get { + if case .sendInputEvent(let v)? = payloadVariant {return v} + return AdminMessage.InputEvent() + } + set {payloadVariant = .sendInputEvent(newValue)} + } + /// /// Set the owner for this node public var setOwner: User { @@ -487,6 +499,26 @@ public struct AdminMessage { set {payloadVariant = .commitEditSettings(newValue)} } + /// + /// Add a contact (User) to the nodedb + public var addContact: SharedContact { + get { + if case .addContact(let v)? = payloadVariant {return v} + return SharedContact() + } + set {payloadVariant = .addContact(newValue)} + } + + /// + /// Initiate or respond to a key verification request + public var keyVerification: KeyVerificationAdmin { + get { + if case .keyVerification(let v)? = payloadVariant {return v} + return KeyVerificationAdmin() + } + set {payloadVariant = .keyVerification(newValue)} + } + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. public var factoryResetDevice: Int32 { @@ -563,7 +595,7 @@ public struct AdminMessage { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send the specified channel in the response to this message /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) @@ -642,6 +674,10 @@ public struct AdminMessage { /// Remove backups of the node's preferences case removeBackupPreferences(AdminMessage.BackupLocation) /// + /// Send an input event to the node. + /// This is used to trigger physical input events like button presses, touch events, etc. + case sendInputEvent(AdminMessage.InputEvent) + /// /// Set the owner for this node case setOwner(User) /// @@ -705,6 +741,12 @@ public struct AdminMessage { /// Commits an open transaction for any edits made to config, module config, owner, and channel settings case commitEditSettings(Bool) /// + /// Add a contact (User) to the nodedb + case addContact(SharedContact) + /// + /// Initiate or respond to a key verification request + case keyVerification(KeyVerificationAdmin) + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. case factoryResetDevice(Int32) /// @@ -728,225 +770,11 @@ public struct AdminMessage { /// Tell the node to reset the nodedb. case nodedbReset(Int32) - #if !swift(>=4.1) - public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.getChannelRequest, .getChannelRequest): return { - guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getChannelResponse, .getChannelResponse): return { - guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerRequest, .getOwnerRequest): return { - guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getOwnerResponse, .getOwnerResponse): return { - guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigRequest, .getConfigRequest): return { - guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getConfigResponse, .getConfigResponse): return { - guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigRequest, .getModuleConfigRequest): return { - guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getModuleConfigResponse, .getModuleConfigResponse): return { - guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return { - guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return { - guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return { - guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return { - guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneRequest, .getRingtoneRequest): return { - guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getRingtoneResponse, .getRingtoneResponse): return { - guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return { - guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return { - guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setHamMode, .setHamMode): return { - guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { - guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { - guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.enterDfuModeRequest, .enterDfuModeRequest): return { - guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deleteFileRequest, .deleteFileRequest): return { - guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setScale, .setScale): return { - guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.backupPreferences, .backupPreferences): return { - guard case .backupPreferences(let l) = lhs, case .backupPreferences(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.restorePreferences, .restorePreferences): return { - guard case .restorePreferences(let l) = lhs, case .restorePreferences(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeBackupPreferences, .removeBackupPreferences): return { - guard case .removeBackupPreferences(let l) = lhs, case .removeBackupPreferences(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setOwner, .setOwner): return { - guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setChannel, .setChannel): return { - guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setConfig, .setConfig): return { - guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setModuleConfig, .setModuleConfig): return { - guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return { - guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setRingtoneMessage, .setRingtoneMessage): return { - guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeByNodenum, .removeByNodenum): return { - guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFavoriteNode, .setFavoriteNode): return { - guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFavoriteNode, .removeFavoriteNode): return { - guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setFixedPosition, .setFixedPosition): return { - guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeFixedPosition, .removeFixedPosition): return { - guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setTimeOnly, .setTimeOnly): return { - guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getUiConfigRequest, .getUiConfigRequest): return { - guard case .getUiConfigRequest(let l) = lhs, case .getUiConfigRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.getUiConfigResponse, .getUiConfigResponse): return { - guard case .getUiConfigResponse(let l) = lhs, case .getUiConfigResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeUiConfig, .storeUiConfig): return { - guard case .storeUiConfig(let l) = lhs, case .storeUiConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.setIgnoredNode, .setIgnoredNode): return { - guard case .setIgnoredNode(let l) = lhs, case .setIgnoredNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.removeIgnoredNode, .removeIgnoredNode): return { - guard case .removeIgnoredNode(let l) = lhs, case .removeIgnoredNode(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.beginEditSettings, .beginEditSettings): return { - guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.commitEditSettings, .commitEditSettings): return { - guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetDevice, .factoryResetDevice): return { - guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootOtaSeconds, .rebootOtaSeconds): return { - guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.exitSimulator, .exitSimulator): return { - guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebootSeconds, .rebootSeconds): return { - guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.shutdownSeconds, .shutdownSeconds): return { - guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.factoryResetConfig, .factoryResetConfig): return { - guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodedbReset, .nodedbReset): return { - guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// TODO: REPLACE - public enum ConfigType: SwiftProtobuf.Enum { + public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1026,11 +854,25 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ConfigType] = [ + .deviceConfig, + .positionConfig, + .powerConfig, + .networkConfig, + .displayConfig, + .loraConfig, + .bluetoothConfig, + .securityConfig, + .sessionkeyConfig, + .deviceuiConfig, + ] + } /// /// TODO: REPLACE - public enum ModuleConfigType: SwiftProtobuf.Enum { + public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1128,9 +970,26 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.ModuleConfigType] = [ + .mqttConfig, + .serialConfig, + .extnotifConfig, + .storeforwardConfig, + .rangetestConfig, + .telemetryConfig, + .cannedmsgConfig, + .audioConfig, + .remotehardwareConfig, + .neighborinfoConfig, + .ambientlightingConfig, + .detectionsensorConfig, + .paxcounterConfig, + ] + } - public enum BackupLocation: SwiftProtobuf.Enum { + public enum BackupLocation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1162,61 +1021,48 @@ public struct AdminMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [AdminMessage.BackupLocation] = [ + .flash, + .sd, + ] + + } + + /// + /// Input event message to be sent to the node. + public struct InputEvent: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// The input event code + public var eventCode: UInt32 = 0 + + /// + /// Keyboard character code + public var kbChar: UInt32 = 0 + + /// + /// The touch X coordinate + public var touchX: UInt32 = 0 + + /// + /// The touch Y coordinate + public var touchY: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} } public init() {} } -#if swift(>=4.2) - -extension AdminMessage.ConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ConfigType] = [ - .deviceConfig, - .positionConfig, - .powerConfig, - .networkConfig, - .displayConfig, - .loraConfig, - .bluetoothConfig, - .securityConfig, - .sessionkeyConfig, - .deviceuiConfig, - ] -} - -extension AdminMessage.ModuleConfigType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.ModuleConfigType] = [ - .mqttConfig, - .serialConfig, - .extnotifConfig, - .storeforwardConfig, - .rangetestConfig, - .telemetryConfig, - .cannedmsgConfig, - .audioConfig, - .remotehardwareConfig, - .neighborinfoConfig, - .ambientlightingConfig, - .detectionsensorConfig, - .paxcounterConfig, - ] -} - -extension AdminMessage.BackupLocation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [AdminMessage.BackupLocation] = [ - .flash, - .sd, - ] -} - -#endif // swift(>=4.2) - /// /// Parameters for setting up Meshtastic for ameteur radio usage -public struct HamParameters { +public struct HamParameters: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1246,7 +1092,7 @@ public struct HamParameters { /// /// Response envelope for node_remote_hardware_pins -public struct NodeRemoteHardwarePinsResponse { +public struct NodeRemoteHardwarePinsResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1260,15 +1106,128 @@ public struct NodeRemoteHardwarePinsResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension AdminMessage: @unchecked Sendable {} -extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension AdminMessage.ConfigType: @unchecked Sendable {} -extension AdminMessage.ModuleConfigType: @unchecked Sendable {} -extension AdminMessage.BackupLocation: @unchecked Sendable {} -extension HamParameters: @unchecked Sendable {} -extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) +public struct SharedContact: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// The node number of the contact + public var nodeNum: UInt32 = 0 + + /// + /// The User of the contact + public var user: User { + get {return _user ?? User()} + set {_user = newValue} + } + /// Returns true if `user` has been explicitly set. + public var hasUser: Bool {return self._user != nil} + /// Clears the value of `user`. Subsequent reads from it will return its default value. + public mutating func clearUser() {self._user = nil} + + /// + /// Add this contact to the blocked / ignored list + public var shouldIgnore: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _user: User? = nil +} + +/// +/// This message is used by a client to initiate or complete a key verification +public struct KeyVerificationAdmin: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var messageType: KeyVerificationAdmin.MessageType = .initiateVerification + + /// + /// The nodenum we're requesting + public var remoteNodenum: UInt32 = 0 + + /// + /// The nonce is used to track the connection + public var nonce: UInt64 = 0 + + /// + /// The 4 digit code generated by the remote node, and communicated outside the mesh + public var securityNumber: UInt32 { + get {return _securityNumber ?? 0} + set {_securityNumber = newValue} + } + /// Returns true if `securityNumber` has been explicitly set. + public var hasSecurityNumber: Bool {return self._securityNumber != nil} + /// Clears the value of `securityNumber`. Subsequent reads from it will return its default value. + public mutating func clearSecurityNumber() {self._securityNumber = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// + /// Three stages of this request. + public enum MessageType: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// This is the first stage, where a client initiates + case initiateVerification // = 0 + + /// + /// After the nonce has been returned over the mesh, the client prompts for the security number + /// And uses this message to provide it to the node. + case provideSecurityNumber // = 1 + + /// + /// Once the user has compared the verification message, this message notifies the node. + case doVerify // = 2 + + /// + /// This is the cancel path, can be taken at any point + case doNotVerify // = 3 + case UNRECOGNIZED(Int) + + public init() { + self = .initiateVerification + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .initiateVerification + case 1: self = .provideSecurityNumber + case 2: self = .doVerify + case 3: self = .doNotVerify + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .initiateVerification: return 0 + case .provideSecurityNumber: return 1 + case .doVerify: return 2 + case .doNotVerify: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [KeyVerificationAdmin.MessageType] = [ + .initiateVerification, + .provideSecurityNumber, + .doVerify, + .doNotVerify, + ] + + } + + public init() {} + + fileprivate var _securityNumber: UInt32? = nil +} // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -1303,6 +1262,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 24: .standard(proto: "backup_preferences"), 25: .standard(proto: "restore_preferences"), 26: .standard(proto: "remove_backup_preferences"), + 27: .standard(proto: "send_input_event"), 32: .standard(proto: "set_owner"), 33: .standard(proto: "set_channel"), 34: .standard(proto: "set_config"), @@ -1322,6 +1282,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 48: .standard(proto: "remove_ignored_node"), 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), + 66: .standard(proto: "add_contact"), + 67: .standard(proto: "key_verification"), 94: .standard(proto: "factory_reset_device"), 95: .standard(proto: "reboot_ota_seconds"), 96: .standard(proto: "exit_simulator"), @@ -1577,6 +1539,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .removeBackupPreferences(v) } }() + case 27: try { + var v: AdminMessage.InputEvent? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .sendInputEvent(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .sendInputEvent(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1764,6 +1739,32 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .commitEditSettings(v) } }() + case 66: try { + var v: SharedContact? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .addContact(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .addContact(v) + } + }() + case 67: try { + var v: KeyVerificationAdmin? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerification(v) + } + }() case 94: try { var v: Int32? try decoder.decodeSingularInt32Field(value: &v) @@ -1932,6 +1933,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .removeBackupPreferences(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularEnumField(value: v, fieldNumber: 26) }() + case .sendInputEvent?: try { + guard case .sendInputEvent(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 27) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) @@ -2008,6 +2013,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .commitEditSettings(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 65) }() + case .addContact?: try { + guard case .addContact(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 66) + }() + case .keyVerification?: try { + guard case .keyVerification(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 67) + }() case .factoryResetDevice?: try { guard case .factoryResetDevice(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 94) @@ -2092,6 +2105,56 @@ extension AdminMessage.BackupLocation: SwiftProtobuf._ProtoNameProviding { ] } +extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = AdminMessage.protoMessageName + ".InputEvent" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "event_code"), + 2: .standard(proto: "kb_char"), + 3: .standard(proto: "touch_x"), + 4: .standard(proto: "touch_y"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.eventCode) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.kbChar) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.touchX) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.touchY) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.eventCode != 0 { + try visitor.visitSingularUInt32Field(value: self.eventCode, fieldNumber: 1) + } + if self.kbChar != 0 { + try visitor.visitSingularUInt32Field(value: self.kbChar, fieldNumber: 2) + } + if self.touchX != 0 { + try visitor.visitSingularUInt32Field(value: self.touchX, fieldNumber: 3) + } + if self.touchY != 0 { + try visitor.visitSingularUInt32Field(value: self.touchY, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: AdminMessage.InputEvent, rhs: AdminMessage.InputEvent) -> Bool { + if lhs.eventCode != rhs.eventCode {return false} + if lhs.kbChar != rhs.kbChar {return false} + if lhs.touchX != rhs.touchX {return false} + if lhs.touchY != rhs.touchY {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".HamParameters" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -2123,7 +2186,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.txPower != 0 { try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2) } - if self.frequency != 0 { + if self.frequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3) } if !self.shortName.isEmpty { @@ -2173,3 +2236,114 @@ extension NodeRemoteHardwarePinsResponse: SwiftProtobuf.Message, SwiftProtobuf._ return true } } + +extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SharedContact" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "node_num"), + 2: .same(proto: "user"), + 3: .standard(proto: "should_ignore"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.nodeNum) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._user) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.shouldIgnore) }() + 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.nodeNum != 0 { + try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1) + } + try { if let v = self._user { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if self.shouldIgnore != false { + try visitor.visitSingularBoolField(value: self.shouldIgnore, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: SharedContact, rhs: SharedContact) -> Bool { + if lhs.nodeNum != rhs.nodeNum {return false} + if lhs._user != rhs._user {return false} + if lhs.shouldIgnore != rhs.shouldIgnore {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationAdmin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationAdmin" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "message_type"), + 2: .standard(proto: "remote_nodenum"), + 3: .same(proto: "nonce"), + 4: .standard(proto: "security_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.messageType) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.remoteNodenum) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self._securityNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.messageType != .initiateVerification { + try visitor.visitSingularEnumField(value: self.messageType, fieldNumber: 1) + } + if self.remoteNodenum != 0 { + try visitor.visitSingularUInt32Field(value: self.remoteNodenum, fieldNumber: 2) + } + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 3) + } + try { if let v = self._securityNumber { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationAdmin, rhs: KeyVerificationAdmin) -> Bool { + if lhs.messageType != rhs.messageType {return false} + if lhs.remoteNodenum != rhs.remoteNodenum {return false} + if lhs.nonce != rhs.nonce {return false} + if lhs._securityNumber != rhs._securityNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationAdmin.MessageType: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "INITIATE_VERIFICATION"), + 1: .same(proto: "PROVIDE_SECURITY_NUMBER"), + 2: .same(proto: "DO_VERIFY"), + 3: .same(proto: "DO_NOT_VERIFY"), + ] +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift index 0457077c..52dac5ca 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/apponly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/apponly.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// any SECONDARY channels. /// No DISABLED channels are included. /// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL -public struct ChannelSet { +public struct ChannelSet: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -53,10 +53,6 @@ public struct ChannelSet { fileprivate var _loraConfig: Config.LoRaConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSet: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index 867648a9..06d6af88 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/atak.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Team: SwiftProtobuf.Enum { +public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -130,11 +131,6 @@ public enum Team: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Team] = [ .unspecifedColor, @@ -153,13 +149,12 @@ extension Team: CaseIterable { .darkGreen, .brown, ] -} -#endif // swift(>=4.2) +} /// /// Role of the group member -public enum MemberRole: SwiftProtobuf.Enum { +public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -233,11 +228,6 @@ public enum MemberRole: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MemberRole] = [ .unspecifed, @@ -250,13 +240,12 @@ extension MemberRole: CaseIterable { .rto, .k9, ] -} -#endif // swift(>=4.2) +} /// /// Packets for the official ATAK Plugin -public struct TAKPacket { +public struct TAKPacket: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -337,7 +326,7 @@ public struct TAKPacket { /// /// The payload of the packet - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TAK position report case pli(PLI) @@ -349,28 +338,6 @@ public struct TAKPacket { /// May be compressed / truncated by the sender (EUD) case detail(Data) - #if !swift(>=4.1) - public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.pli, .pli): return { - guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.chat, .chat): return { - guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detail, .detail): return { - guard case .detail(let l) = lhs, case .detail(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -382,7 +349,7 @@ public struct TAKPacket { /// /// ATAK GeoChat message -public struct GeoChat { +public struct GeoChat: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -424,7 +391,7 @@ public struct GeoChat { /// /// ATAK Group /// <__group role='Team Member' name='Cyan'/> -public struct Group { +public struct Group: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -446,7 +413,7 @@ public struct Group { /// /// ATAK EUD Status /// -public struct Status { +public struct Status: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -463,7 +430,7 @@ public struct Status { /// /// ATAK Contact /// -public struct Contact { +public struct Contact: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -483,7 +450,7 @@ public struct Contact { /// /// Position Location Information from ATAK -public struct PLI { +public struct PLI: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -515,18 +482,6 @@ public struct PLI { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Team: @unchecked Sendable {} -extension MemberRole: @unchecked Sendable {} -extension TAKPacket: @unchecked Sendable {} -extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension GeoChat: @unchecked Sendable {} -extension Group: @unchecked Sendable {} -extension Status: @unchecked Sendable {} -extension Contact: @unchecked Sendable {} -extension PLI: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift index 1b8c84de..ce1f0503 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/cannedmessages.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/cannedmessages.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct CannedMessageModuleConfig { +public struct CannedMessageModuleConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -36,10 +36,6 @@ public struct CannedMessageModuleConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension CannedMessageModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift index 5b9c7e49..180cd698 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/channel.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/channel.proto @@ -36,13 +37,15 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example -public struct ChannelSettings { +public struct ChannelSettings: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// /// Deprecated in favor of LoraConfig.channel_num + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var channelNum: UInt32 = 0 /// @@ -111,7 +114,7 @@ public struct ChannelSettings { /// /// This message is specifically for modules to store per-channel configuration data. -public struct ModuleSettings { +public struct ModuleSettings: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -132,7 +135,7 @@ public struct ModuleSettings { /// /// A pair of a channel number, mode and the (sharable) settings for that channel -public struct Channel { +public struct Channel: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -170,7 +173,7 @@ public struct Channel { /// cross band routing as needed. /// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time /// (but any number of SECONDARY channels can't be sent received on that common frequency) - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -209,6 +212,13 @@ public struct Channel { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Channel.Role] = [ + .disabled, + .primary, + .secondary, + ] + } public init() {} @@ -216,26 +226,6 @@ public struct Channel { fileprivate var _settings: ChannelSettings? = nil } -#if swift(>=4.2) - -extension Channel.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Channel.Role] = [ - .disabled, - .primary, - .secondary, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension ChannelSettings: @unchecked Sendable {} -extension ModuleSettings: @unchecked Sendable {} -extension Channel: @unchecked Sendable {} -extension Channel.Role: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift index f89a8e3c..d72c0ae1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/clientonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/clientonly.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -23,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This abstraction is used to contain any configuration for provisioning a node on any client. /// It is useful for importing and exporting configurations. -public struct DeviceProfile { +public struct DeviceProfile: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -130,10 +130,6 @@ public struct DeviceProfile { fileprivate var _cannedMessages: String? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceProfile: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index e0d45bcf..e37bc908 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/config.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Config { +public struct Config: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -113,7 +114,7 @@ public struct Config { /// /// Payload Variant - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { case device(Config.DeviceConfig) case position(Config.PositionConfig) case power(Config.PowerConfig) @@ -125,61 +126,11 @@ public struct Config { case sessionkey(Config.SessionkeyConfig) case deviceUi(DeviceUIConfig) - #if !swift(>=4.1) - public static func ==(lhs: Config.OneOf_PayloadVariant, rhs: Config.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.device, .device): return { - guard case .device(let l) = lhs, case .device(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.position, .position): return { - guard case .position(let l) = lhs, case .position(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.power, .power): return { - guard case .power(let l) = lhs, case .power(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.network, .network): return { - guard case .network(let l) = lhs, case .network(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.display, .display): return { - guard case .display(let l) = lhs, case .display(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lora, .lora): return { - guard case .lora(let l) = lhs, case .lora(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.bluetooth, .bluetooth): return { - guard case .bluetooth(let l) = lhs, case .bluetooth(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.security, .security): return { - guard case .security(let l) = lhs, case .security(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.sessionkey, .sessionkey): return { - guard case .sessionkey(let l) = lhs, case .sessionkey(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deviceUi, .deviceUi): return { - guard case .deviceUi(let l) = lhs, case .deviceUi(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// Configuration - public struct DeviceConfig { + public struct DeviceConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -191,6 +142,8 @@ public struct Config { /// /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var serialEnabled: Bool = false /// @@ -220,6 +173,8 @@ public struct Config { /// If true, device is considered to be "managed" by a mesh administrator /// Clients should then limit available configuration and administrative options inside the user interface /// Moved to SecurityConfig + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var isManaged: Bool = false /// @@ -234,11 +189,16 @@ public struct Config { /// If true, disable the default blinking LED (LED_PIN) behavior on the device public var ledHeartbeatDisabled: Bool = false + /// + /// Controls buzzer behavior for audio feedback + /// Defaults to ENABLED + public var buzzerMode: Config.DeviceConfig.BuzzerMode = .allEnabled + public var unknownFields = SwiftProtobuf.UnknownStorage() /// /// Defines the device's role on the Mesh network - public enum Role: SwiftProtobuf.Enum { + public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -256,6 +216,8 @@ public struct Config { /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 + + /// NOTE: This enum value was marked as deprecated in the .proto file case routerClient // = 3 /// @@ -356,11 +318,27 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.Role] = [ + .client, + .clientMute, + .router, + .routerClient, + .repeater, + .tracker, + .sensor, + .tak, + .clientHidden, + .lostAndFound, + .takTracker, + .routerLate, + ] + } /// /// Defines the device's behavior for how messages are rebroadcast - public enum RebroadcastMode: SwiftProtobuf.Enum { + public enum RebroadcastMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -421,6 +399,77 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + .all, + .allSkipDecoding, + .localOnly, + .knownOnly, + .none, + .corePortnumsOnly, + ] + + } + + /// + /// Defines buzzer behavior for audio feedback + public enum BuzzerMode: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// Default behavior. + /// Buzzer is enabled for all audio feedback including button presses and alerts. + case allEnabled // = 0 + + /// + /// Disabled. + /// All buzzer audio feedback is disabled. + case disabled // = 1 + + /// + /// Notifications Only. + /// Buzzer is enabled only for notifications and alerts, but not for button presses. + /// External notification config determines the specifics of the notification behavior. + case notificationsOnly // = 2 + + /// + /// 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 + case UNRECOGNIZED(Int) + + public init() { + self = .allEnabled + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .allEnabled + case 1: self = .disabled + case 2: self = .notificationsOnly + case 3: self = .systemOnly + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .allEnabled: return 0 + case .disabled: return 1 + case .notificationsOnly: return 2 + case .systemOnly: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DeviceConfig.BuzzerMode] = [ + .allEnabled, + .disabled, + .notificationsOnly, + .systemOnly, + ] + } public init() {} @@ -428,7 +477,7 @@ public struct Config { /// /// Position Config - public struct PositionConfig { + public struct PositionConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -450,6 +499,8 @@ public struct Config { /// /// Is GPS enabled for this node? + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsEnabled: Bool = false /// @@ -460,6 +511,8 @@ public struct Config { /// /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var gpsAttemptTime: UInt32 = 0 /// @@ -500,7 +553,7 @@ public struct Config { /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss - public enum PositionFlags: SwiftProtobuf.Enum { + public enum PositionFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -590,9 +643,24 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.PositionFlags] = [ + .unset, + .altitude, + .altitudeMsl, + .geoidalSeparation, + .dop, + .hvdop, + .satinview, + .seqNo, + .timestamp, + .heading, + .speed, + ] + } - public enum GpsMode: SwiftProtobuf.Enum { + public enum GpsMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -630,6 +698,13 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] + } public init() {} @@ -638,7 +713,7 @@ public struct Config { /// /// Power Config\ /// See [Power Config](/docs/settings/config/power) for additional power config details. - public struct PowerConfig { + public struct PowerConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -698,7 +773,7 @@ public struct Config { /// /// Network Config - public struct NetworkConfig { + public struct NetworkConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -747,9 +822,13 @@ public struct Config { /// Flags for enabling/disabling network protocols public var enabledProtocols: UInt32 = 0 + /// + /// Enable/Disable ipv6 support + public var ipv6Enabled: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum AddressMode: SwiftProtobuf.Enum { + public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -781,11 +860,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.AddressMode] = [ + .dhcp, + .static, + ] + } /// /// Available flags auxiliary network protocols - public enum ProtocolFlags: SwiftProtobuf.Enum { + public enum ProtocolFlags: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -817,9 +902,15 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.NetworkConfig.ProtocolFlags] = [ + .noBroadcast, + .udpBroadcast, + ] + } - public struct IpV4Config { + public struct IpV4Config: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -852,7 +943,7 @@ public struct Config { /// /// Display Config - public struct DisplayConfig { + public struct DisplayConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -913,7 +1004,7 @@ public struct Config { /// /// How the GPS coordinates are displayed on the OLED screen. - public enum GpsCoordinateFormat: SwiftProtobuf.Enum { + public enum GpsCoordinateFormat: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -976,11 +1067,21 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + .dec, + .dms, + .utm, + .mgrs, + .olc, + .osgr, + ] + } /// /// Unit display preference - public enum DisplayUnits: SwiftProtobuf.Enum { + public enum DisplayUnits: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1012,11 +1113,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + .metric, + .imperial, + ] + } /// /// Override OLED outo detect with this if it fails. - public enum OledType: SwiftProtobuf.Enum { + public enum OledType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1066,9 +1173,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.OledType] = [ + .oledAuto, + .oledSsd1306, + .oledSh1106, + .oledSh1107, + .oledSh110712864, + ] + } - public enum DisplayMode: SwiftProtobuf.Enum { + public enum DisplayMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1112,9 +1228,17 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.DisplayMode] = [ + .default, + .twocolor, + .inverted, + .color, + ] + } - public enum CompassOrientation: SwiftProtobuf.Enum { + public enum CompassOrientation: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1182,6 +1306,18 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] + } public init() {} @@ -1189,7 +1325,7 @@ public struct Config { /// /// Lora Config - public struct LoRaConfig { + public struct LoRaConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1353,7 +1489,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum RegionCode: SwiftProtobuf.Enum { + public enum RegionCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1505,12 +1641,38 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.RegionCode] = [ + .unset, + .us, + .eu433, + .eu868, + .cn, + .jp, + .anz, + .kr, + .tw, + .ru, + .in, + .nz865, + .th, + .lora24, + .ua433, + .ua868, + .my433, + .my919, + .sg923, + .ph433, + .ph868, + .ph915, + ] + } /// /// Standard predefined channel settings /// Note: these mappings must match ModemPreset Choice in the device code. - public enum ModemPreset: SwiftProtobuf.Enum { + public enum ModemPreset: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1524,6 +1686,8 @@ public struct Config { /// /// Very Long Range - Slow /// Deprecated in 2.5: Works only with txco and is unusably slow + /// + /// NOTE: This enum value was marked as deprecated in the .proto file case veryLongSlow // = 2 /// @@ -1587,6 +1751,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.LoRaConfig.ModemPreset] = [ + .longFast, + .longSlow, + .veryLongSlow, + .mediumSlow, + .mediumFast, + .shortSlow, + .shortFast, + .longModerate, + .shortTurbo, + ] + } public init() {} @@ -1594,7 +1771,7 @@ public struct Config { fileprivate var _storage = _StorageClass.defaultInstance } - public struct BluetoothConfig { + public struct BluetoothConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1613,7 +1790,7 @@ public struct Config { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum PairingMode: SwiftProtobuf.Enum { + public enum PairingMode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1651,12 +1828,19 @@ public struct Config { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Config.BluetoothConfig.PairingMode] = [ + .randomPin, + .fixedPin, + .noPin, + ] + } public init() {} } - public struct SecurityConfig { + public struct SecurityConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1700,7 +1884,7 @@ public struct Config { /// /// Blank config request, strictly for getting the session key - public struct SessionkeyConfig { + public struct SessionkeyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1713,218 +1897,6 @@ public struct Config { public init() {} } -#if swift(>=4.2) - -extension Config.DeviceConfig.Role: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.Role] = [ - .client, - .clientMute, - .router, - .routerClient, - .repeater, - .tracker, - .sensor, - .tak, - .clientHidden, - .lostAndFound, - .takTracker, - .routerLate, - ] -} - -extension Config.DeviceConfig.RebroadcastMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ - .all, - .allSkipDecoding, - .localOnly, - .knownOnly, - .none, - .corePortnumsOnly, - ] -} - -extension Config.PositionConfig.PositionFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.PositionFlags] = [ - .unset, - .altitude, - .altitudeMsl, - .geoidalSeparation, - .dop, - .hvdop, - .satinview, - .seqNo, - .timestamp, - .heading, - .speed, - ] -} - -extension Config.PositionConfig.GpsMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.PositionConfig.GpsMode] = [ - .disabled, - .enabled, - .notPresent, - ] -} - -extension Config.NetworkConfig.AddressMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.AddressMode] = [ - .dhcp, - .static, - ] -} - -extension Config.NetworkConfig.ProtocolFlags: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.NetworkConfig.ProtocolFlags] = [ - .noBroadcast, - .udpBroadcast, - ] -} - -extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ - .dec, - .dms, - .utm, - .mgrs, - .olc, - .osgr, - ] -} - -extension Config.DisplayConfig.DisplayUnits: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayUnits] = [ - .metric, - .imperial, - ] -} - -extension Config.DisplayConfig.OledType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.OledType] = [ - .oledAuto, - .oledSsd1306, - .oledSh1106, - .oledSh1107, - .oledSh110712864, - ] -} - -extension Config.DisplayConfig.DisplayMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.DisplayMode] = [ - .default, - .twocolor, - .inverted, - .color, - ] -} - -extension Config.DisplayConfig.CompassOrientation: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.DisplayConfig.CompassOrientation] = [ - .degrees0, - .degrees90, - .degrees180, - .degrees270, - .degrees0Inverted, - .degrees90Inverted, - .degrees180Inverted, - .degrees270Inverted, - ] -} - -extension Config.LoRaConfig.RegionCode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.RegionCode] = [ - .unset, - .us, - .eu433, - .eu868, - .cn, - .jp, - .anz, - .kr, - .tw, - .ru, - .in, - .nz865, - .th, - .lora24, - .ua433, - .ua868, - .my433, - .my919, - .sg923, - .ph433, - .ph868, - .ph915, - ] -} - -extension Config.LoRaConfig.ModemPreset: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.LoRaConfig.ModemPreset] = [ - .longFast, - .longSlow, - .veryLongSlow, - .mediumSlow, - .mediumFast, - .shortSlow, - .shortFast, - .longModerate, - .shortTurbo, - ] -} - -extension Config.BluetoothConfig.PairingMode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Config.BluetoothConfig.PairingMode] = [ - .randomPin, - .fixedPin, - .noPin, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension Config: @unchecked Sendable {} -extension Config.OneOf_PayloadVariant: @unchecked Sendable {} -extension Config.DeviceConfig: @unchecked Sendable {} -extension Config.DeviceConfig.Role: @unchecked Sendable {} -extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} -extension Config.PositionConfig: @unchecked Sendable {} -extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} -extension Config.PositionConfig.GpsMode: @unchecked Sendable {} -extension Config.PowerConfig: @unchecked Sendable {} -extension Config.NetworkConfig: @unchecked Sendable {} -extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} -extension Config.NetworkConfig.ProtocolFlags: @unchecked Sendable {} -extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {} -extension Config.DisplayConfig: @unchecked Sendable {} -extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} -extension Config.DisplayConfig.OledType: @unchecked Sendable {} -extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} -extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} -extension Config.LoRaConfig: @unchecked Sendable {} -extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} -extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} -extension Config.BluetoothConfig: @unchecked Sendable {} -extension Config.BluetoothConfig.PairingMode: @unchecked Sendable {} -extension Config.SecurityConfig: @unchecked Sendable {} -extension Config.SessionkeyConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -2157,6 +2129,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 10: .standard(proto: "disable_triple_click"), 11: .same(proto: "tzdef"), 12: .standard(proto: "led_heartbeat_disabled"), + 13: .standard(proto: "buzzer_mode"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2176,6 +2149,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }() case 11: try { try decoder.decodeSingularStringField(value: &self.tzdef) }() case 12: try { try decoder.decodeSingularBoolField(value: &self.ledHeartbeatDisabled) }() + case 13: try { try decoder.decodeSingularEnumField(value: &self.buzzerMode) }() default: break } } @@ -2215,6 +2189,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.ledHeartbeatDisabled != false { try visitor.visitSingularBoolField(value: self.ledHeartbeatDisabled, fieldNumber: 12) } + if self.buzzerMode != .allEnabled { + try visitor.visitSingularEnumField(value: self.buzzerMode, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -2230,6 +2207,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.disableTripleClick != rhs.disableTripleClick {return false} if lhs.tzdef != rhs.tzdef {return false} if lhs.ledHeartbeatDisabled != rhs.ledHeartbeatDisabled {return false} + if lhs.buzzerMode != rhs.buzzerMode {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2263,6 +2241,15 @@ extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding ] } +extension Config.DeviceConfig.BuzzerMode: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "ALL_ENABLED"), + 1: .same(proto: "DISABLED"), + 2: .same(proto: "NOTIFICATIONS_ONLY"), + 3: .same(proto: "SYSTEM_ONLY"), + ] +} + extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = Config.protoMessageName + ".PositionConfig" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -2432,7 +2419,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.onBatteryShutdownAfterSecs != 0 { try visitor.visitSingularUInt32Field(value: self.onBatteryShutdownAfterSecs, fieldNumber: 2) } - if self.adcMultiplierOverride != 0 { + if self.adcMultiplierOverride.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.adcMultiplierOverride, fieldNumber: 3) } if self.waitBluetoothSecs != 0 { @@ -2483,6 +2470,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp 8: .standard(proto: "ipv4_config"), 9: .standard(proto: "rsyslog_server"), 10: .standard(proto: "enabled_protocols"), + 11: .standard(proto: "ipv6_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2500,6 +2488,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 8: try { try decoder.decodeSingularMessageField(value: &self._ipv4Config) }() case 9: try { try decoder.decodeSingularStringField(value: &self.rsyslogServer) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.enabledProtocols) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.ipv6Enabled) }() default: break } } @@ -2537,6 +2526,9 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.enabledProtocols != 0 { try visitor.visitSingularUInt32Field(value: self.enabledProtocols, fieldNumber: 10) } + if self.ipv6Enabled != false { + try visitor.visitSingularBoolField(value: self.ipv6Enabled, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -2550,6 +2542,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs._ipv4Config != rhs._ipv4Config {return false} if lhs.rsyslogServer != rhs.rsyslogServer {return false} if lhs.enabledProtocols != rhs.enabledProtocols {return false} + if lhs.ipv6Enabled != rhs.ipv6Enabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2900,7 +2893,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._codingRate != 0 { try visitor.visitSingularUInt32Field(value: _storage._codingRate, fieldNumber: 5) } - if _storage._frequencyOffset != 0 { + if _storage._frequencyOffset.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._frequencyOffset, fieldNumber: 6) } if _storage._region != .unset { @@ -2924,7 +2917,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._sx126XRxBoostedGain != false { try visitor.visitSingularBoolField(value: _storage._sx126XRxBoostedGain, fieldNumber: 13) } - if _storage._overrideFrequency != 0 { + if _storage._overrideFrequency.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._overrideFrequency, fieldNumber: 14) } if _storage._paFanDisabled != false { @@ -3141,8 +3134,8 @@ extension Config.SessionkeyConfig: SwiftProtobuf.Message, SwiftProtobuf._Message public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift index a2ec180e..6847c0e3 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/connection_status.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/connection_status.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct DeviceConnectionStatus { +public struct DeviceConnectionStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -81,7 +81,7 @@ public struct DeviceConnectionStatus { /// /// WiFi connection status -public struct WifiConnectionStatus { +public struct WifiConnectionStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -114,7 +114,7 @@ public struct WifiConnectionStatus { /// /// Ethernet connection status -public struct EthernetConnectionStatus { +public struct EthernetConnectionStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -139,7 +139,7 @@ public struct EthernetConnectionStatus { /// /// Ethernet or WiFi connection status -public struct NetworkConnectionStatus { +public struct NetworkConnectionStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -167,7 +167,7 @@ public struct NetworkConnectionStatus { /// /// Bluetooth connection status -public struct BluetoothConnectionStatus { +public struct BluetoothConnectionStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -191,7 +191,7 @@ public struct BluetoothConnectionStatus { /// /// Serial connection status -public struct SerialConnectionStatus { +public struct SerialConnectionStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -209,15 +209,6 @@ public struct SerialConnectionStatus { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension DeviceConnectionStatus: @unchecked Sendable {} -extension WifiConnectionStatus: @unchecked Sendable {} -extension EthernetConnectionStatus: @unchecked Sendable {} -extension NetworkConnectionStatus: @unchecked Sendable {} -extension BluetoothConnectionStatus: @unchecked Sendable {} -extension SerialConnectionStatus: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift index c3835518..9607abe1 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/device_ui.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/device_ui.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum Theme: SwiftProtobuf.Enum { +public enum Theme: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +59,18 @@ public enum Theme: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Theme: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Theme] = [ .dark, .light, .red, ] -} -#endif // swift(>=4.2) +} /// /// Localization -public enum Language: SwiftProtobuf.Enum { +public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -146,6 +141,10 @@ public enum Language: SwiftProtobuf.Enum { /// Ukrainian case ukrainian // = 16 + /// + /// Bulgarian + case bulgarian // = 17 + /// /// Simplified Chinese (experimental) case simplifiedChinese // = 30 @@ -178,6 +177,7 @@ public enum Language: SwiftProtobuf.Enum { case 14: self = .norwegian case 15: self = .slovenian case 16: self = .ukrainian + case 17: self = .bulgarian case 30: self = .simplifiedChinese case 31: self = .traditionalChinese default: self = .UNRECOGNIZED(rawValue) @@ -203,17 +203,13 @@ public enum Language: SwiftProtobuf.Enum { case .norwegian: return 14 case .slovenian: return 15 case .ukrainian: return 16 + case .bulgarian: return 17 case .simplifiedChinese: return 30 case .traditionalChinese: return 31 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension Language: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Language] = [ .english, @@ -233,14 +229,14 @@ extension Language: CaseIterable { .norwegian, .slovenian, .ukrainian, + .bulgarian, .simplifiedChinese, .traditionalChinese, ] + } -#endif // swift(>=4.2) - -public struct DeviceUIConfig { +public struct DeviceUIConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -361,7 +357,7 @@ public struct DeviceUIConfig { fileprivate var _storage = _StorageClass.defaultInstance } -public struct NodeFilter { +public struct NodeFilter: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -399,7 +395,7 @@ public struct NodeFilter { public init() {} } -public struct NodeHighlight { +public struct NodeHighlight: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -429,7 +425,7 @@ public struct NodeHighlight { public init() {} } -public struct GeoPoint { +public struct GeoPoint: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -451,7 +447,7 @@ public struct GeoPoint { public init() {} } -public struct Map { +public struct Map: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -482,16 +478,6 @@ public struct Map { fileprivate var _home: GeoPoint? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension Theme: @unchecked Sendable {} -extension Language: @unchecked Sendable {} -extension DeviceUIConfig: @unchecked Sendable {} -extension NodeFilter: @unchecked Sendable {} -extension NodeHighlight: @unchecked Sendable {} -extension GeoPoint: @unchecked Sendable {} -extension Map: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -523,6 +509,7 @@ extension Language: SwiftProtobuf._ProtoNameProviding { 14: .same(proto: "NORWEGIAN"), 15: .same(proto: "SLOVENIAN"), 16: .same(proto: "UKRAINIAN"), + 17: .same(proto: "BULGARIAN"), 30: .same(proto: "SIMPLIFIED_CHINESE"), 31: .same(proto: "TRADITIONAL_CHINESE"), ] diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index 9a5dfe8f..acbc9682 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/deviceonly.proto @@ -22,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Position with static location information only for NodeDBLite -public struct PositionLite { +public struct PositionLite: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -57,13 +58,15 @@ public struct PositionLite { public init() {} } -public struct UserLite { +public struct UserLite: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// /// This is the addr of the radio. + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -97,12 +100,25 @@ public struct UserLite { /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() + /// + /// Whether or not the node can be messaged + public var isUnmessagable: Bool { + get {return _isUnmessagable ?? false} + set {_isUnmessagable = newValue} + } + /// Returns true if `isUnmessagable` has been explicitly set. + public var hasIsUnmessagable: Bool {return self._isUnmessagable != nil} + /// Clears the value of `isUnmessagable`. Subsequent reads from it will return its default value. + public mutating func clearIsUnmessagable() {self._isUnmessagable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _isUnmessagable: Bool? = nil } -public struct NodeInfoLite { +public struct NodeInfoLite: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -211,6 +227,14 @@ public struct NodeInfoLite { set {_uniqueStorage()._nextHop = newValue} } + /// + /// Bitfield for storing booleans. + /// LSB 0 is_key_manually_verified + public var bitfield: UInt32 { + get {return _storage._bitfield} + set {_uniqueStorage()._bitfield = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -224,7 +248,7 @@ public struct NodeInfoLite { /// FIXME, since we write this each time we enter deep sleep (and have infinite /// flash) it would be better to use some sort of append only data structure for /// the receive queue and use the preferences store for the other stuff -public struct DeviceState { +public struct DeviceState: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -284,6 +308,8 @@ public struct DeviceState { /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. /// Deprecated in 2.3.1 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} @@ -292,6 +318,8 @@ public struct DeviceState { /// /// Previously used to manage GPS factory resets. /// Deprecated in 2.5.23 + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var didGpsReset: Bool { get {return _storage._didGpsReset} set {_uniqueStorage()._didGpsReset = newValue} @@ -324,7 +352,7 @@ public struct DeviceState { fileprivate var _storage = _StorageClass.defaultInstance } -public struct NodeDatabase { +public struct NodeDatabase: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -346,7 +374,7 @@ public struct NodeDatabase { /// /// The on-disk saved channels -public struct ChannelFile { +public struct ChannelFile: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -368,7 +396,7 @@ public struct ChannelFile { /// /// The on-disk backup of the node's preferences -public struct BackupPreferences { +public struct BackupPreferences: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -435,16 +463,6 @@ public struct BackupPreferences { fileprivate var _owner: User? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension PositionLite: @unchecked Sendable {} -extension UserLite: @unchecked Sendable {} -extension NodeInfoLite: @unchecked Sendable {} -extension DeviceState: @unchecked Sendable {} -extension NodeDatabase: @unchecked Sendable {} -extension ChannelFile: @unchecked Sendable {} -extension BackupPreferences: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -515,6 +533,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 5: .standard(proto: "is_licensed"), 6: .same(proto: "role"), 7: .standard(proto: "public_key"), + 9: .standard(proto: "is_unmessagable"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -530,12 +549,17 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 5: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 7: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self._isUnmessagable) }() 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.macaddr.isEmpty { try visitor.visitSingularBytesField(value: self.macaddr, fieldNumber: 1) } @@ -557,6 +581,9 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if !self.publicKey.isEmpty { try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 7) } + try { if let v = self._isUnmessagable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -568,6 +595,7 @@ extension UserLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} if lhs.publicKey != rhs.publicKey {return false} + if lhs._isUnmessagable != rhs._isUnmessagable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -588,6 +616,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 10: .standard(proto: "is_favorite"), 11: .standard(proto: "is_ignored"), 12: .standard(proto: "next_hop"), + 13: .same(proto: "bitfield"), ] fileprivate class _StorageClass { @@ -603,6 +632,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _isFavorite: Bool = false var _isIgnored: Bool = false var _nextHop: UInt32 = 0 + var _bitfield: UInt32 = 0 #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -629,6 +659,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _isFavorite = source._isFavorite _isIgnored = source._isIgnored _nextHop = source._nextHop + _bitfield = source._bitfield } } @@ -659,6 +690,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &_storage._nextHop) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &_storage._bitfield) }() default: break } } @@ -680,7 +712,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -707,6 +739,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._nextHop != 0 { try visitor.visitSingularUInt32Field(value: _storage._nextHop, fieldNumber: 12) } + if _storage._bitfield != 0 { + try visitor.visitSingularUInt32Field(value: _storage._bitfield, fieldNumber: 13) + } } try unknownFields.traverse(visitor: &visitor) } @@ -728,6 +763,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} if _storage._nextHop != rhs_storage._nextHop {return false} + if _storage._bitfield != rhs_storage._bitfield {return false} return true } if !storagesAreEqual {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift index 92b72c15..165ed685 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/interdevice.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/interdevice.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum MessageType: SwiftProtobuf.Enum { +public enum MessageType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case ack // = 0 @@ -82,11 +82,6 @@ public enum MessageType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension MessageType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [MessageType] = [ .ack, @@ -102,11 +97,10 @@ extension MessageType: CaseIterable { .aht20Humidity, .tvocIndex, ] + } -#endif // swift(>=4.2) - -public struct SensorData { +public struct SensorData: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -136,34 +130,16 @@ public struct SensorData { public var unknownFields = SwiftProtobuf.UnknownStorage() /// The sensor data, either as a float or an uint32 - public enum OneOf_Data: Equatable { + public enum OneOf_Data: Equatable, Sendable { case floatValue(Float) case uint32Value(UInt32) - #if !swift(>=4.1) - public static func ==(lhs: SensorData.OneOf_Data, rhs: SensorData.OneOf_Data) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.floatValue, .floatValue): return { - guard case .floatValue(let l) = lhs, case .floatValue(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.uint32Value, .uint32Value): return { - guard case .uint32Value(let l) = lhs, case .uint32Value(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -public struct InterdeviceMessage { +public struct InterdeviceMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -190,41 +166,15 @@ public struct InterdeviceMessage { public var unknownFields = SwiftProtobuf.UnknownStorage() /// The message data - public enum OneOf_Data: Equatable { + public enum OneOf_Data: Equatable, Sendable { case nmea(String) case sensor(SensorData) - #if !swift(>=4.1) - public static func ==(lhs: InterdeviceMessage.OneOf_Data, rhs: InterdeviceMessage.OneOf_Data) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.nmea, .nmea): return { - guard case .nmea(let l) = lhs, case .nmea(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.sensor, .sensor): return { - guard case .sensor(let l) = lhs, case .sensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension MessageType: @unchecked Sendable {} -extension SensorData: @unchecked Sendable {} -extension SensorData.OneOf_Data: @unchecked Sendable {} -extension InterdeviceMessage: @unchecked Sendable {} -extension InterdeviceMessage.OneOf_Data: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift index 0af27466..c3356286 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/localonly.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/localonly.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct LocalConfig { +public struct LocalConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -129,7 +129,7 @@ public struct LocalConfig { fileprivate var _storage = _StorageClass.defaultInstance } -public struct LocalModuleConfig { +public struct LocalModuleConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -293,11 +293,6 @@ public struct LocalModuleConfig { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=5.5) && canImport(_Concurrency) -extension LocalConfig: @unchecked Sendable {} -extension LocalModuleConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 051f395f..85981376 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/mesh.proto @@ -25,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// bin/build-all.sh script. /// Because they will be used to find firmware filenames in the android app for OTA updates. /// To match the old style filenames, _ is converted to -, p is converted to . -public enum HardwareModel: SwiftProtobuf.Enum { +public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -441,6 +442,34 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin case crowpanel // = 97 + ///* + /// Lilygo LINK32 board with sensors + case link32 // = 98 + + ///* + /// Seeed Tracker L1 + case seeedWioTrackerL1 // = 99 + + ///* + /// Seeed Tracker L1 EINK driver + case seeedWioTrackerL1Eink // = 100 + + /// + /// Reserved ID for future and past use + case qwantzTinyArms // = 101 + + ///* + /// Lilygo T-Deck Pro + case tDeckPro // = 102 + + ///* + /// Lilygo TLora Pager + case tLoraPager // = 103 + + ///* + /// GAT562 Mesh Trial Tracker + case gat562MeshTrialTracker // = 104 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -552,6 +581,13 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 95: self = .seeedSolarNode case 96: self = .nomadstarMeteorPro case 97: self = .crowpanel + case 98: self = .link32 + case 99: self = .seeedWioTrackerL1 + case 100: self = .seeedWioTrackerL1Eink + case 101: self = .qwantzTinyArms + case 102: self = .tDeckPro + case 103: self = .tLoraPager + case 104: self = .gat562MeshTrialTracker case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -657,16 +693,18 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .seeedSolarNode: return 95 case .nomadstarMeteorPro: return 96 case .crowpanel: return 97 + case .link32: return 98 + case .seeedWioTrackerL1: return 99 + case .seeedWioTrackerL1Eink: return 100 + case .qwantzTinyArms: return 101 + case .tDeckPro: return 102 + case .tLoraPager: return 103 + case .gat562MeshTrialTracker: return 104 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [HardwareModel] = [ .unset, @@ -767,15 +805,21 @@ extension HardwareModel: CaseIterable { .seeedSolarNode, .nomadstarMeteorPro, .crowpanel, + .link32, + .seeedWioTrackerL1, + .seeedWioTrackerL1Eink, + .qwantzTinyArms, + .tDeckPro, + .tLoraPager, + .gat562MeshTrialTracker, .privateHw, ] -} -#endif // swift(>=4.2) +} /// /// Shared constants between device and phone -public enum Constants: SwiftProtobuf.Enum { +public enum Constants: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -810,26 +854,20 @@ public enum Constants: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Constants] = [ .zero, .dataPayloadLen, ] -} -#endif // swift(>=4.2) +} /// /// Error codes for critical errors /// The device might report these fault codes on the screen. /// If you encounter a fault code, please post on the meshtastic.discourse.group /// and we'll try to help. -public enum CriticalErrorCode: SwiftProtobuf.Enum { +public enum CriticalErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -938,11 +976,6 @@ public enum CriticalErrorCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [CriticalErrorCode] = [ .none, @@ -960,15 +993,14 @@ extension CriticalErrorCode: CaseIterable { .flashCorruptionRecoverable, .flashCorruptionUnrecoverable, ] -} -#endif // swift(>=4.2) +} /// /// Enum for modules excluded from a device's configuration. /// Each value represents a ModuleConfigType that can be toggled as excluded /// by setting its corresponding bit in the `excluded_modules` bitmask field. -public enum ExcludedModules: SwiftProtobuf.Enum { +public enum ExcludedModules: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1084,11 +1116,6 @@ public enum ExcludedModules: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension ExcludedModules: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ExcludedModules] = [ .excludedNone, @@ -1108,13 +1135,12 @@ extension ExcludedModules: CaseIterable { .bluetoothConfig, .networkConfig, ] -} -#endif // swift(>=4.2) +} /// /// A GPS Position -public struct Position { +public struct Position: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1331,7 +1357,7 @@ public struct Position { /// /// How the location was acquired: manual, onboard GPS, external (EUD) GPS - public enum LocSource: SwiftProtobuf.Enum { + public enum LocSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1375,12 +1401,20 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.LocSource] = [ + .locUnset, + .locManual, + .locInternal, + .locExternal, + ] + } /// /// How the altitude was acquired: manual, GPS int/ext, etc /// Default: same as location_source if present - public enum AltSource: SwiftProtobuf.Enum { + public enum AltSource: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1430,6 +1464,15 @@ public struct Position { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Position.AltSource] = [ + .altUnset, + .altManual, + .altInternal, + .altExternal, + .altBarometric, + ] + } public init() {} @@ -1437,31 +1480,6 @@ public struct Position { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension Position.LocSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.LocSource] = [ - .locUnset, - .locManual, - .locInternal, - .locExternal, - ] -} - -extension Position.AltSource: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Position.AltSource] = [ - .altUnset, - .altManual, - .altInternal, - .altExternal, - .altBarometric, - ] -} - -#endif // swift(>=4.2) - /// /// Broadcast when a newly powered mesh node wants to find a node num it can use /// Sent from the phone over bluetooth to set the user id for the owner of this node. @@ -1483,7 +1501,7 @@ extension Position.AltSource: CaseIterable { /// A few nodenums are reserved and will never be requested: /// 0xff - broadcast /// 0 through 3 - for future use -public struct User { +public struct User: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1508,6 +1526,8 @@ public struct User { /// Deprecated in Meshtastic 2.1.x /// This is the addr of the radio. /// Not populated by the phone, but added by the esp32 when broadcasting + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var macaddr: Data = Data() /// @@ -1532,14 +1552,27 @@ public struct User { /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. public var publicKey: Data = Data() + /// + /// Whether or not the node can be messaged + public var isUnmessagable: Bool { + get {return _isUnmessagable ?? false} + set {_isUnmessagable = newValue} + } + /// Returns true if `isUnmessagable` has been explicitly set. + public var hasIsUnmessagable: Bool {return self._isUnmessagable != nil} + /// Clears the value of `isUnmessagable`. Subsequent reads from it will return its default value. + public mutating func clearIsUnmessagable() {self._isUnmessagable = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _isUnmessagable: Bool? = nil } /// /// A message used in a traceroute -public struct RouteDiscovery { +public struct RouteDiscovery: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1567,7 +1600,7 @@ public struct RouteDiscovery { /// /// A Routing control Data packet handled by the routing module -public struct Routing { +public struct Routing: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1607,7 +1640,7 @@ public struct Routing { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// A route request going from the requester case routeRequest(RouteDiscovery) @@ -1619,34 +1652,12 @@ public struct Routing { /// in addition to ack.fail_id to provide details on the type of failure). case errorReason(Routing.Error) - #if !swift(>=4.1) - public static func ==(lhs: Routing.OneOf_Variant, rhs: Routing.OneOf_Variant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.routeRequest, .routeRequest): return { - guard case .routeRequest(let l) = lhs, case .routeRequest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.routeReply, .routeReply): return { - guard case .routeReply(let l) = lhs, case .routeReply(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorReason, .errorReason): return { - guard case .errorReason(let l) = lhs, case .errorReason(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide /// details on the type of failure). - public enum Error: SwiftProtobuf.Enum { + public enum Error: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1764,42 +1775,36 @@ public struct Routing { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Routing.Error] = [ + .none, + .noRoute, + .gotNak, + .timeout, + .noInterface, + .maxRetransmit, + .noChannel, + .tooLarge, + .noResponse, + .dutyCycleLimit, + .badRequest, + .notAuthorized, + .pkiFailed, + .pkiUnknownPubkey, + .adminBadSessionKey, + .adminPublicKeyUnauthorized, + ] + } public init() {} } -#if swift(>=4.2) - -extension Routing.Error: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Routing.Error] = [ - .none, - .noRoute, - .gotNak, - .timeout, - .noInterface, - .maxRetransmit, - .noChannel, - .tooLarge, - .noResponse, - .dutyCycleLimit, - .badRequest, - .notAuthorized, - .pkiFailed, - .pkiUnknownPubkey, - .adminBadSessionKey, - .adminPublicKeyUnauthorized, - ] -} - -#endif // swift(>=4.2) - /// /// (Formerly called SubPacket) /// The payload portion fo a packet, this is the actual bytes that are sent /// inside a radio packet (because from/to are broken out by the comms library) -public struct DataMessage { +public struct DataMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1864,9 +1869,34 @@ public struct DataMessage { fileprivate var _bitfield: UInt32? = nil } +/// +/// The actual over-the-mesh message doing KeyVerification +public struct KeyVerification: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// random value Selected by the requesting node + public var nonce: UInt64 = 0 + + /// + /// The final authoritative hash, only to be sent by NodeA at the end of the handshake + public var hash1: Data = Data() + + /// + /// The intermediary hash (actually derived from hash1), + /// sent from NodeB to NodeA in response to the initial message. + public var hash2: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Waypoint message, used to share arbitrary locations across the mesh -public struct Waypoint { +public struct Waypoint: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1928,7 +1958,7 @@ public struct Waypoint { /// /// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server -public struct MqttClientProxyMessage { +public struct MqttClientProxyMessage: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1969,7 +1999,7 @@ public struct MqttClientProxyMessage { /// /// The actual service envelope payload or text for mqtt pub / sub - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// Bytes case data(Data) @@ -1977,24 +2007,6 @@ public struct MqttClientProxyMessage { /// Text case text(String) - #if !swift(>=4.1) - public static func ==(lhs: MqttClientProxyMessage.OneOf_PayloadVariant, rhs: MqttClientProxyMessage.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.data, .data): return { - guard case .data(let l) = lhs, case .data(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -2004,7 +2016,7 @@ public struct MqttClientProxyMessage { /// A packet envelope sent/received over the mesh /// only payload_variant is sent in the payload portion of the LORA packet. /// The other fields are either not sent at all, or sent in the special 16 byte LORA header. -public struct MeshPacket { +public struct MeshPacket: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -2138,6 +2150,8 @@ public struct MeshPacket { /// /// Describe if this message is delayed + /// + /// NOTE: This field was marked as deprecated in the .proto file. public var delayed: MeshPacket.Delayed { get {return _storage._delayed} set {_uniqueStorage()._delayed = newValue} @@ -2199,7 +2213,7 @@ public struct MeshPacket { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case decoded(DataMessage) @@ -2207,24 +2221,6 @@ public struct MeshPacket { /// TODO: REPLACE case encrypted(Data) - #if !swift(>=4.1) - public static func ==(lhs: MeshPacket.OneOf_PayloadVariant, rhs: MeshPacket.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.decoded, .decoded): return { - guard case .decoded(let l) = lhs, case .decoded(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.encrypted, .encrypted): return { - guard case .encrypted(let l) = lhs, case .encrypted(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// @@ -2246,7 +2242,7 @@ public struct MeshPacket { /// So I bit the bullet and implemented a new (internal - not sent over the air) /// field in MeshPacket called 'priority'. /// And the transmission queue in the router object is now a priority queue. - public enum Priority: SwiftProtobuf.Enum { + public enum Priority: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2330,11 +2326,25 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Priority] = [ + .unset, + .min, + .background, + .default, + .reliable, + .response, + .high, + .alert, + .ack, + .max, + ] + } /// /// Identify if this is a delayed packet - public enum Delayed: SwiftProtobuf.Enum { + public enum Delayed: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2372,6 +2382,13 @@ public struct MeshPacket { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [MeshPacket.Delayed] = [ + .noDelay, + .broadcast, + .direct, + ] + } public init() {} @@ -2379,35 +2396,6 @@ public struct MeshPacket { fileprivate var _storage = _StorageClass.defaultInstance } -#if swift(>=4.2) - -extension MeshPacket.Priority: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Priority] = [ - .unset, - .min, - .background, - .default, - .reliable, - .response, - .high, - .alert, - .ack, - .max, - ] -} - -extension MeshPacket.Delayed: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [MeshPacket.Delayed] = [ - .noDelay, - .broadcast, - .direct, - ] -} - -#endif // swift(>=4.2) - /// /// The bluetooth to device link: /// Old BTLE protocol docs from TODO, merge in above and make real docs... @@ -2425,7 +2413,7 @@ extension MeshPacket.Delayed: CaseIterable { /// level etc) SET_CONFIG (switches device to a new set of radio params and /// preshared key, drops all existing nodes, force our node to rejoin this new group) /// Full information about a node on the mesh -public struct NodeInfo { +public struct NodeInfo: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -2527,6 +2515,15 @@ public struct NodeInfo { set {_uniqueStorage()._isIgnored = newValue} } + /// + /// True if node public key has been verified. + /// Persists between NodeDB internal clean ups + /// LSB 0 of the bitfield + public var isKeyManuallyVerified: Bool { + get {return _storage._isKeyManuallyVerified} + set {_uniqueStorage()._isKeyManuallyVerified = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -2538,7 +2535,7 @@ public struct NodeInfo { /// Unique local debugging info for this node /// Note: we don't include position or the user info, because that will come in the /// Sent to the phone in response to WantNodes. -public struct MyNodeInfo { +public struct MyNodeInfo: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -2577,7 +2574,7 @@ public struct MyNodeInfo { /// on the message it is assumed to be a continuation of the previously sent message. /// This allows the device code to use fixed maxlen 64 byte strings for messages, /// and then extend as needed by emitting multiple records. -public struct LogRecord { +public struct LogRecord: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -2602,7 +2599,7 @@ public struct LogRecord { /// /// Log levels, chosen to match python logging conventions. - public enum Level: SwiftProtobuf.Enum { + public enum Level: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -2664,29 +2661,23 @@ public struct LogRecord { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [LogRecord.Level] = [ + .unset, + .critical, + .error, + .warning, + .info, + .debug, + .trace, + ] + } public init() {} } -#if swift(>=4.2) - -extension LogRecord.Level: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [LogRecord.Level] = [ - .unset, - .critical, - .error, - .warning, - .info, - .debug, - .trace, - ] -} - -#endif // swift(>=4.2) - -public struct QueueStatus { +public struct QueueStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -2713,7 +2704,7 @@ public struct QueueStatus { /// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? /// It will sit in that descriptor until consumed by the phone, /// at which point the next item in the FIFO will be populated. -public struct FromRadio { +public struct FromRadio: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -2899,7 +2890,7 @@ public struct FromRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Log levels, chosen to match python logging conventions. case packet(MeshPacket) @@ -2957,80 +2948,6 @@ public struct FromRadio { /// Persistent data for device-ui case deviceuiConfig(DeviceUIConfig) - #if !swift(>=4.1) - public static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.myInfo, .myInfo): return { - guard case .myInfo(let l) = lhs, case .myInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.nodeInfo, .nodeInfo): return { - guard case .nodeInfo(let l) = lhs, case .nodeInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.config, .config): return { - guard case .config(let l) = lhs, case .config(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.logRecord, .logRecord): return { - guard case .logRecord(let l) = lhs, case .logRecord(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.configCompleteID, .configCompleteID): return { - guard case .configCompleteID(let l) = lhs, case .configCompleteID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rebooted, .rebooted): return { - guard case .rebooted(let l) = lhs, case .rebooted(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.moduleConfig, .moduleConfig): return { - guard case .moduleConfig(let l) = lhs, case .moduleConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.channel, .channel): return { - guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.queueStatus, .queueStatus): return { - guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.metadata, .metadata): return { - guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileInfo, .fileInfo): return { - guard case .fileInfo(let l) = lhs, case .fileInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.clientNotification, .clientNotification): return { - guard case .clientNotification(let l) = lhs, case .clientNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.deviceuiConfig, .deviceuiConfig): return { - guard case .deviceuiConfig(let l) = lhs, case .deviceuiConfig(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -3041,7 +2958,7 @@ public struct FromRadio { /// To be used for important messages that should to be displayed to the user /// in the form of push notifications or validation messages when saving /// invalid configuration. -public struct ClientNotification { +public struct ClientNotification: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3069,16 +2986,135 @@ public struct ClientNotification { /// The message body of the notification public var message: String = String() + public var payloadVariant: ClientNotification.OneOf_PayloadVariant? = nil + + public var keyVerificationNumberInform: KeyVerificationNumberInform { + get { + if case .keyVerificationNumberInform(let v)? = payloadVariant {return v} + return KeyVerificationNumberInform() + } + set {payloadVariant = .keyVerificationNumberInform(newValue)} + } + + public var keyVerificationNumberRequest: KeyVerificationNumberRequest { + get { + if case .keyVerificationNumberRequest(let v)? = payloadVariant {return v} + return KeyVerificationNumberRequest() + } + set {payloadVariant = .keyVerificationNumberRequest(newValue)} + } + + public var keyVerificationFinal: KeyVerificationFinal { + get { + if case .keyVerificationFinal(let v)? = payloadVariant {return v} + return KeyVerificationFinal() + } + set {payloadVariant = .keyVerificationFinal(newValue)} + } + + public var duplicatedPublicKey: DuplicatedPublicKey { + get { + if case .duplicatedPublicKey(let v)? = payloadVariant {return v} + return DuplicatedPublicKey() + } + set {payloadVariant = .duplicatedPublicKey(newValue)} + } + + public var lowEntropyKey: LowEntropyKey { + get { + if case .lowEntropyKey(let v)? = payloadVariant {return v} + return LowEntropyKey() + } + set {payloadVariant = .lowEntropyKey(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() + public enum OneOf_PayloadVariant: Equatable, Sendable { + case keyVerificationNumberInform(KeyVerificationNumberInform) + case keyVerificationNumberRequest(KeyVerificationNumberRequest) + case keyVerificationFinal(KeyVerificationFinal) + case duplicatedPublicKey(DuplicatedPublicKey) + case lowEntropyKey(LowEntropyKey) + + } + public init() {} fileprivate var _replyID: UInt32? = nil } +public struct KeyVerificationNumberInform: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var securityNumber: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct KeyVerificationNumberRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct KeyVerificationFinal: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var isSender: Bool = false + + public var verificationCharacters: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct DuplicatedPublicKey: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct LowEntropyKey: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Individual File info for the device -public struct FileInfo { +public struct FileInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3099,7 +3135,7 @@ public struct FileInfo { /// /// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. /// Once the write completes the phone can assume it is handled. -public struct ToRadio { +public struct ToRadio: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3179,7 +3215,7 @@ public struct ToRadio { /// /// Log levels, chosen to match python logging conventions. - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Send this packet on the mesh case packet(MeshPacket) @@ -3206,40 +3242,6 @@ public struct ToRadio { /// Heartbeat message (used to keep the device connection awake on serial) case heartbeat(Heartbeat) - #if !swift(>=4.1) - public static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.packet, .packet): return { - guard case .packet(let l) = lhs, case .packet(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.wantConfigID, .wantConfigID): return { - guard case .wantConfigID(let l) = lhs, case .wantConfigID(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.disconnect, .disconnect): return { - guard case .disconnect(let l) = lhs, case .disconnect(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xmodemPacket, .xmodemPacket): return { - guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mqttClientProxyMessage, .mqttClientProxyMessage): return { - guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -3247,7 +3249,7 @@ public struct ToRadio { /// /// Compressed message payload -public struct Compressed { +public struct Compressed: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3267,7 +3269,7 @@ public struct Compressed { /// /// Full info on edges for a single node -public struct NeighborInfo { +public struct NeighborInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3295,7 +3297,7 @@ public struct NeighborInfo { /// /// A single edge in the mesh -public struct Neighbor { +public struct Neighbor: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3325,7 +3327,7 @@ public struct Neighbor { /// /// Device metadata response -public struct DeviceMetadata { +public struct DeviceMetadata: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3387,7 +3389,7 @@ public struct DeviceMetadata { /// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. -public struct Heartbeat { +public struct Heartbeat: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3399,7 +3401,7 @@ public struct Heartbeat { /// /// RemoteHardwarePins associated with a node -public struct NodeRemoteHardwarePin { +public struct NodeRemoteHardwarePin: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3426,7 +3428,7 @@ public struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } -public struct ChunkedPayload { +public struct ChunkedPayload: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3454,7 +3456,7 @@ public struct ChunkedPayload { /// /// Wrapper message for broken repeated oneof support -public struct resend_chunks { +public struct resend_chunks: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3468,7 +3470,7 @@ public struct resend_chunks { /// /// Responses to a ChunkedPayload request -public struct ChunkedPayloadResponse { +public struct ChunkedPayloadResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -3511,7 +3513,7 @@ public struct ChunkedPayloadResponse { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// Request to transfer chunked payload case requestTransfer(Bool) @@ -3522,77 +3524,11 @@ public struct ChunkedPayloadResponse { /// Request missing indexes in the chunked payload case resendChunks(resend_chunks) - #if !swift(>=4.1) - public static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.requestTransfer, .requestTransfer): return { - guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.acceptTransfer, .acceptTransfer): return { - guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.resendChunks, .resendChunks): return { - guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareModel: @unchecked Sendable {} -extension Constants: @unchecked Sendable {} -extension CriticalErrorCode: @unchecked Sendable {} -extension ExcludedModules: @unchecked Sendable {} -extension Position: @unchecked Sendable {} -extension Position.LocSource: @unchecked Sendable {} -extension Position.AltSource: @unchecked Sendable {} -extension User: @unchecked Sendable {} -extension RouteDiscovery: @unchecked Sendable {} -extension Routing: @unchecked Sendable {} -extension Routing.OneOf_Variant: @unchecked Sendable {} -extension Routing.Error: @unchecked Sendable {} -extension DataMessage: @unchecked Sendable {} -extension Waypoint: @unchecked Sendable {} -extension MqttClientProxyMessage: @unchecked Sendable {} -extension MqttClientProxyMessage.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket: @unchecked Sendable {} -extension MeshPacket.OneOf_PayloadVariant: @unchecked Sendable {} -extension MeshPacket.Priority: @unchecked Sendable {} -extension MeshPacket.Delayed: @unchecked Sendable {} -extension NodeInfo: @unchecked Sendable {} -extension MyNodeInfo: @unchecked Sendable {} -extension LogRecord: @unchecked Sendable {} -extension LogRecord.Level: @unchecked Sendable {} -extension QueueStatus: @unchecked Sendable {} -extension FromRadio: @unchecked Sendable {} -extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension ClientNotification: @unchecked Sendable {} -extension FileInfo: @unchecked Sendable {} -extension ToRadio: @unchecked Sendable {} -extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {} -extension Compressed: @unchecked Sendable {} -extension NeighborInfo: @unchecked Sendable {} -extension Neighbor: @unchecked Sendable {} -extension DeviceMetadata: @unchecked Sendable {} -extension Heartbeat: @unchecked Sendable {} -extension NodeRemoteHardwarePin: @unchecked Sendable {} -extension ChunkedPayload: @unchecked Sendable {} -extension resend_chunks: @unchecked Sendable {} -extension ChunkedPayloadResponse: @unchecked Sendable {} -extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -3697,6 +3633,13 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 95: .same(proto: "SEEED_SOLAR_NODE"), 96: .same(proto: "NOMADSTAR_METEOR_PRO"), 97: .same(proto: "CROWPANEL"), + 98: .same(proto: "LINK_32"), + 99: .same(proto: "SEEED_WIO_TRACKER_L1"), + 100: .same(proto: "SEEED_WIO_TRACKER_L1_EINK"), + 101: .same(proto: "QWANTZ_TINY_ARMS"), + 102: .same(proto: "T_DECK_PRO"), + 103: .same(proto: "T_LORA_PAGER"), + 104: .same(proto: "GAT562_MESH_TRIAL_TRACKER"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -4030,6 +3973,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, 6: .standard(proto: "is_licensed"), 7: .same(proto: "role"), 8: .standard(proto: "public_key"), + 9: .standard(proto: "is_unmessagable"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4046,12 +3990,17 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 8: try { try decoder.decodeSingularBytesField(value: &self.publicKey) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self._isUnmessagable) }() 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.id.isEmpty { try visitor.visitSingularStringField(value: self.id, fieldNumber: 1) } @@ -4076,6 +4025,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if !self.publicKey.isEmpty { try visitor.visitSingularBytesField(value: self.publicKey, fieldNumber: 8) } + try { if let v = self._isUnmessagable { + try visitor.visitSingularBoolField(value: v, fieldNumber: 9) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -4088,6 +4040,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if lhs.isLicensed != rhs.isLicensed {return false} if lhs.role != rhs.role {return false} if lhs.publicKey != rhs.publicKey {return false} + if lhs._isUnmessagable != rhs._isUnmessagable {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4331,6 +4284,50 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati } } +extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerification" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .same(proto: "hash1"), + 3: .same(proto: "hash2"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.hash1) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.hash2) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.hash1.isEmpty { + try visitor.visitSingularBytesField(value: self.hash1, fieldNumber: 2) + } + if !self.hash2.isEmpty { + try visitor.visitSingularBytesField(value: self.hash2, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerification, rhs: KeyVerification) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.hash1 != rhs.hash1 {return false} + if lhs.hash2 != rhs.hash2 {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Waypoint" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -4654,7 +4651,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxTime != 0 { try visitor.visitSingularFixed32Field(value: _storage._rxTime, fieldNumber: 7) } - if _storage._rxSnr != 0 { + if _storage._rxSnr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._rxSnr, fieldNumber: 8) } if _storage._hopLimit != 0 { @@ -4767,6 +4764,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 9: .standard(proto: "hops_away"), 10: .standard(proto: "is_favorite"), 11: .standard(proto: "is_ignored"), + 12: .standard(proto: "is_key_manually_verified"), ] fileprivate class _StorageClass { @@ -4781,6 +4779,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _hopsAway: UInt32? = nil var _isFavorite: Bool = false var _isIgnored: Bool = false + var _isKeyManuallyVerified: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -4806,6 +4805,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _hopsAway = source._hopsAway _isFavorite = source._isFavorite _isIgnored = source._isIgnored + _isKeyManuallyVerified = source._isKeyManuallyVerified } } @@ -4835,6 +4835,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() + case 12: try { try decoder.decodeSingularBoolField(value: &_storage._isKeyManuallyVerified) }() default: break } } @@ -4856,7 +4857,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = _storage._position { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if _storage._snr != 0 { + if _storage._snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) } if _storage._lastHeard != 0 { @@ -4880,6 +4881,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._isIgnored != false { try visitor.visitSingularBoolField(value: _storage._isIgnored, fieldNumber: 11) } + if _storage._isKeyManuallyVerified != false { + try visitor.visitSingularBoolField(value: _storage._isKeyManuallyVerified, fieldNumber: 12) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4900,6 +4904,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._hopsAway != rhs_storage._hopsAway {return false} if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} + if _storage._isKeyManuallyVerified != rhs_storage._isKeyManuallyVerified {return false} return true } if !storagesAreEqual {return false} @@ -5402,6 +5407,11 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 2: .same(proto: "time"), 3: .same(proto: "level"), 4: .same(proto: "message"), + 11: .standard(proto: "key_verification_number_inform"), + 12: .standard(proto: "key_verification_number_request"), + 13: .standard(proto: "key_verification_final"), + 14: .standard(proto: "duplicated_public_key"), + 15: .standard(proto: "low_entropy_key"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -5414,6 +5424,71 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 2: try { try decoder.decodeSingularFixed32Field(value: &self.time) }() case 3: try { try decoder.decodeSingularEnumField(value: &self.level) }() case 4: try { try decoder.decodeSingularStringField(value: &self.message) }() + case 11: try { + var v: KeyVerificationNumberInform? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationNumberInform(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationNumberInform(v) + } + }() + case 12: try { + var v: KeyVerificationNumberRequest? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationNumberRequest(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationNumberRequest(v) + } + }() + case 13: try { + var v: KeyVerificationFinal? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationFinal(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationFinal(v) + } + }() + case 14: try { + var v: DuplicatedPublicKey? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .duplicatedPublicKey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .duplicatedPublicKey(v) + } + }() + case 15: try { + var v: LowEntropyKey? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .lowEntropyKey(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .lowEntropyKey(v) + } + }() default: break } } @@ -5436,6 +5511,29 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if !self.message.isEmpty { try visitor.visitSingularStringField(value: self.message, fieldNumber: 4) } + switch self.payloadVariant { + case .keyVerificationNumberInform?: try { + guard case .keyVerificationNumberInform(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .keyVerificationNumberRequest?: try { + guard case .keyVerificationNumberRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .keyVerificationFinal?: try { + guard case .keyVerificationFinal(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case .duplicatedPublicKey?: try { + guard case .duplicatedPublicKey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case .lowEntropyKey?: try { + guard case .lowEntropyKey(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 15) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } @@ -5444,6 +5542,177 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.time != rhs.time {return false} if lhs.level != rhs.level {return false} if lhs.message != rhs.message {return false} + if lhs.payloadVariant != rhs.payloadVariant {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationNumberInform: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationNumberInform" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .standard(proto: "remote_longname"), + 3: .standard(proto: "security_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.securityNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + if self.securityNumber != 0 { + try visitor.visitSingularUInt32Field(value: self.securityNumber, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationNumberInform, rhs: KeyVerificationNumberInform) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.securityNumber != rhs.securityNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationNumberRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationNumberRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .standard(proto: "remote_longname"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationNumberRequest, rhs: KeyVerificationNumberRequest) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationFinal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationFinal" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .standard(proto: "remote_longname"), + 3: .same(proto: "isSender"), + 4: .standard(proto: "verification_characters"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.isSender) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.verificationCharacters) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + if self.isSender != false { + try visitor.visitSingularBoolField(value: self.isSender, fieldNumber: 3) + } + if !self.verificationCharacters.isEmpty { + try visitor.visitSingularStringField(value: self.verificationCharacters, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationFinal, rhs: KeyVerificationFinal) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.isSender != rhs.isSender {return false} + if lhs.verificationCharacters != rhs.verificationCharacters {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension DuplicatedPublicKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".DuplicatedPublicKey" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: DuplicatedPublicKey, rhs: DuplicatedPublicKey) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension LowEntropyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LowEntropyKey" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: LowEntropyKey, rhs: LowEntropyKey) -> Bool { if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -5735,7 +6004,7 @@ extension Neighbor: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if self.nodeID != 0 { try visitor.visitSingularUInt32Field(value: self.nodeID, fieldNumber: 1) } - if self.snr != 0 { + if self.snr.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 2) } if self.lastRxTime != 0 { @@ -5860,8 +6129,8 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index bcf4041c..c2e81366 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/module_config.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -20,7 +20,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public enum RemoteHardwarePinType: SwiftProtobuf.Enum { +public enum RemoteHardwarePinType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -58,24 +58,18 @@ public enum RemoteHardwarePinType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, ] -} -#endif // swift(>=4.2) +} /// /// Module Config -public struct ModuleConfig { +public struct ModuleConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -218,7 +212,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum OneOf_PayloadVariant: Equatable { + public enum OneOf_PayloadVariant: Equatable, Sendable { /// /// TODO: REPLACE case mqtt(ModuleConfig.MQTTConfig) @@ -259,73 +253,11 @@ public struct ModuleConfig { /// TODO: REPLACE case paxcounter(ModuleConfig.PaxcounterConfig) - #if !swift(>=4.1) - public static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.mqtt, .mqtt): return { - guard case .mqtt(let l) = lhs, case .mqtt(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.serial, .serial): return { - guard case .serial(let l) = lhs, case .serial(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.externalNotification, .externalNotification): return { - guard case .externalNotification(let l) = lhs, case .externalNotification(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.storeForward, .storeForward): return { - guard case .storeForward(let l) = lhs, case .storeForward(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rangeTest, .rangeTest): return { - guard case .rangeTest(let l) = lhs, case .rangeTest(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.telemetry, .telemetry): return { - guard case .telemetry(let l) = lhs, case .telemetry(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.cannedMessage, .cannedMessage): return { - guard case .cannedMessage(let l) = lhs, case .cannedMessage(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.audio, .audio): return { - guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.remoteHardware, .remoteHardware): return { - guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.neighborInfo, .neighborInfo): return { - guard case .neighborInfo(let l) = lhs, case .neighborInfo(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.ambientLighting, .ambientLighting): return { - guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.detectionSensor, .detectionSensor): return { - guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.paxcounter, .paxcounter): return { - guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// MQTT Client Config - public struct MQTTConfig { + public struct MQTTConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -400,7 +332,7 @@ public struct ModuleConfig { /// /// Settings for reporting unencrypted information about our node to a map via MQTT - public struct MapReportSettings { + public struct MapReportSettings: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -413,6 +345,10 @@ public struct ModuleConfig { /// Bits of precision for the location sent (default of 32 is full precision). public var positionPrecision: UInt32 = 0 + /// + /// Whether we have opted-in to report our location to the map + public var shouldReportLocation: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -420,7 +356,7 @@ public struct ModuleConfig { /// /// RemoteHardwareModule Config - public struct RemoteHardwareConfig { + public struct RemoteHardwareConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -444,7 +380,7 @@ public struct ModuleConfig { /// /// NeighborInfoModule Config - public struct NeighborInfoConfig { + public struct NeighborInfoConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -470,7 +406,7 @@ public struct ModuleConfig { /// /// Detection Sensor Module Config - public struct DetectionSensorConfig { + public struct DetectionSensorConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -517,7 +453,7 @@ public struct ModuleConfig { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum TriggerType: SwiftProtobuf.Enum { + public enum TriggerType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// Event is triggered if pin is low @@ -569,6 +505,16 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ + .logicLow, + .logicHigh, + .fallingEdge, + .risingEdge, + .eitherEdgeActiveLow, + .eitherEdgeActiveHigh, + ] + } public init() {} @@ -576,7 +522,7 @@ public struct ModuleConfig { /// /// Audio Config for codec2 voice - public struct AudioConfig { + public struct AudioConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -613,7 +559,7 @@ public struct ModuleConfig { /// /// Baudrate for codec2 voice - public enum Audio_Baud: SwiftProtobuf.Enum { + public enum Audio_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case codec2Default // = 0 case codec23200 // = 1 @@ -660,6 +606,19 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + .codec2Default, + .codec23200, + .codec22400, + .codec21600, + .codec21400, + .codec21300, + .codec21200, + .codec2700, + .codec2700B, + ] + } public init() {} @@ -667,7 +626,7 @@ public struct ModuleConfig { /// /// Config for the Paxcounter Module - public struct PaxcounterConfig { + public struct PaxcounterConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -693,7 +652,7 @@ public struct ModuleConfig { /// /// Serial Config - public struct SerialConfig { + public struct SerialConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -736,7 +695,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum Serial_Baud: SwiftProtobuf.Enum { + public enum Serial_Baud: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case baudDefault // = 0 case baud110 // = 1 @@ -804,11 +763,31 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + .baudDefault, + .baud110, + .baud300, + .baud600, + .baud1200, + .baud2400, + .baud4800, + .baud9600, + .baud19200, + .baud38400, + .baud57600, + .baud115200, + .baud230400, + .baud460800, + .baud576000, + .baud921600, + ] + } /// /// TODO: REPLACE - public enum Serial_Mode: SwiftProtobuf.Enum { + public enum Serial_Mode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case `default` // = 0 case simple // = 1 @@ -821,6 +800,10 @@ public struct ModuleConfig { /// Ecowitt WS85 weather station case ws85 // = 6 + + /// VE.Direct is a serial protocol used by Victron Energy products + /// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable + case veDirect // = 7 case UNRECOGNIZED(Int) public init() { @@ -836,6 +819,7 @@ public struct ModuleConfig { case 4: self = .nmea case 5: self = .caltopo case 6: self = .ws85 + case 7: self = .veDirect default: self = .UNRECOGNIZED(rawValue) } } @@ -849,10 +833,23 @@ public struct ModuleConfig { case .nmea: return 4 case .caltopo: return 5 case .ws85: return 6 + case .veDirect: return 7 case .UNRECOGNIZED(let i): return i } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + .default, + .simple, + .proto, + .textmsg, + .nmea, + .caltopo, + .ws85, + .veDirect, + ] + } public init() {} @@ -860,7 +857,7 @@ public struct ModuleConfig { /// /// External Notifications Config - public struct ExternalNotificationConfig { + public struct ExternalNotificationConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -943,7 +940,7 @@ public struct ModuleConfig { /// /// Store and Forward Module Config - public struct StoreForwardConfig { + public struct StoreForwardConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -979,7 +976,7 @@ public struct ModuleConfig { /// /// Preferences for the RangeTestModule - public struct RangeTestConfig { + public struct RangeTestConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1004,7 +1001,7 @@ public struct ModuleConfig { /// /// Configuration for both device and environment metrics - public struct TelemetryConfig { + public struct TelemetryConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1073,7 +1070,7 @@ public struct ModuleConfig { /// /// Canned Messages Module Config - public struct CannedMessageConfig { + public struct CannedMessageConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1128,7 +1125,7 @@ public struct ModuleConfig { /// /// TODO: REPLACE - public enum InputEventChar: SwiftProtobuf.Enum { + public enum InputEventChar: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -1196,6 +1193,18 @@ public struct ModuleConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + .none, + .up, + .down, + .left, + .right, + .select, + .back, + .cancel, + ] + } public init() {} @@ -1204,7 +1213,7 @@ public struct ModuleConfig { /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. - public struct AmbientLightingConfig { + public struct AmbientLightingConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1237,89 +1246,9 @@ public struct ModuleConfig { public init() {} } -#if swift(>=4.2) - -extension ModuleConfig.DetectionSensorConfig.TriggerType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ - .logicLow, - .logicHigh, - .fallingEdge, - .risingEdge, - .eitherEdgeActiveLow, - .eitherEdgeActiveHigh, - ] -} - -extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ - .codec2Default, - .codec23200, - .codec22400, - .codec21600, - .codec21400, - .codec21300, - .codec21200, - .codec2700, - .codec2700B, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ - .baudDefault, - .baud110, - .baud300, - .baud600, - .baud1200, - .baud2400, - .baud4800, - .baud9600, - .baud19200, - .baud38400, - .baud57600, - .baud115200, - .baud230400, - .baud460800, - .baud576000, - .baud921600, - ] -} - -extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ - .default, - .simple, - .proto, - .textmsg, - .nmea, - .caltopo, - .ws85, - ] -} - -extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ - .none, - .up, - .down, - .left, - .right, - .select, - .back, - .cancel, - ] -} - -#endif // swift(>=4.2) - /// /// A GPIO pin definition for remote hardware module -public struct RemoteHardwarePin { +public struct RemoteHardwarePin: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1341,32 +1270,6 @@ public struct RemoteHardwarePin { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RemoteHardwarePinType: @unchecked Sendable {} -extension ModuleConfig: @unchecked Sendable {} -extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} -extension ModuleConfig.MQTTConfig: @unchecked Sendable {} -extension ModuleConfig.MapReportSettings: @unchecked Sendable {} -extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} -extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} -extension ModuleConfig.DetectionSensorConfig.TriggerType: @unchecked Sendable {} -extension ModuleConfig.AudioConfig: @unchecked Sendable {} -extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} -extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} -extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} -extension ModuleConfig.ExternalNotificationConfig: @unchecked Sendable {} -extension ModuleConfig.StoreForwardConfig: @unchecked Sendable {} -extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} -extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} -extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} -extension ModuleConfig.AmbientLightingConfig: @unchecked Sendable {} -extension RemoteHardwarePin: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1748,6 +1651,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "publish_interval_secs"), 2: .standard(proto: "position_precision"), + 3: .standard(proto: "should_report_location"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1758,6 +1662,7 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.publishIntervalSecs) }() case 2: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.shouldReportLocation) }() default: break } } @@ -1770,12 +1675,16 @@ extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._ if self.positionPrecision != 0 { try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 2) } + if self.shouldReportLocation != false { + try visitor.visitSingularBoolField(value: self.shouldReportLocation, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: ModuleConfig.MapReportSettings, rhs: ModuleConfig.MapReportSettings) -> Bool { if lhs.publishIntervalSecs != rhs.publishIntervalSecs {return false} if lhs.positionPrecision != rhs.positionPrecision {return false} + if lhs.shouldReportLocation != rhs.shouldReportLocation {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2190,6 +2099,7 @@ extension ModuleConfig.SerialConfig.Serial_Mode: SwiftProtobuf._ProtoNameProvidi 4: .same(proto: "NMEA"), 5: .same(proto: "CALTOPO"), 6: .same(proto: "WS85"), + 7: .same(proto: "VE_DIRECT"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift index efe6cdd5..80508b5d 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mqtt.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/mqtt.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. -public struct ServiceEnvelope { +public struct ServiceEnvelope: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -57,7 +57,7 @@ public struct ServiceEnvelope { /// /// Information about a node intended to be reported unencrypted to a map using MQTT. -public struct MapReport { +public struct MapReport: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -116,16 +116,16 @@ public struct MapReport { /// Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) public var numOnlineLocalNodes: UInt32 = 0 + /// + /// User has opted in to share their location (map report) with the mqtt server + /// Controlled by map_report.should_report_location + public var hasOptedReportLocation_p: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension ServiceEnvelope: @unchecked Sendable {} -extension MapReport: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -194,6 +194,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 11: .same(proto: "altitude"), 12: .standard(proto: "position_precision"), 13: .standard(proto: "num_online_local_nodes"), + 14: .standard(proto: "has_opted_report_location"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -215,6 +216,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation case 11: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() case 13: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineLocalNodes) }() + case 14: try { try decoder.decodeSingularBoolField(value: &self.hasOptedReportLocation_p) }() default: break } } @@ -260,6 +262,9 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation if self.numOnlineLocalNodes != 0 { try visitor.visitSingularUInt32Field(value: self.numOnlineLocalNodes, fieldNumber: 13) } + if self.hasOptedReportLocation_p != false { + try visitor.visitSingularBoolField(value: self.hasOptedReportLocation_p, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -277,6 +282,7 @@ extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation if lhs.altitude != rhs.altitude {return false} if lhs.positionPrecision != rhs.positionPrecision {return false} if lhs.numOnlineLocalNodes != rhs.numOnlineLocalNodes {return false} + if lhs.hasOptedReportLocation_p != rhs.hasOptedReportLocation_p {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift index cf8aa463..e24ed371 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/paxcount.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/paxcount.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct Paxcount { +public struct Paxcount: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -44,10 +44,6 @@ public struct Paxcount { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Paxcount: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index 3b0efa08..182e233c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/portnums.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -33,7 +33,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: This was formerly a Type enum named 'typ' with the same id # /// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. /// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. -public enum PortNum: SwiftProtobuf.Enum { +public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -111,6 +111,10 @@ public enum PortNum: SwiftProtobuf.Enum { /// Same as Text Message but used for critical alerts. case alertApp // = 11 + /// + /// Module/port for handling key verification requests. + case keyVerificationApp // = 12 + /// /// Provides a 'ping' service that replies to any packet it receives. /// Also serves as a small example module. @@ -232,6 +236,7 @@ public enum PortNum: SwiftProtobuf.Enum { case 9: self = .audioApp case 10: self = .detectionSensorApp case 11: self = .alertApp + case 12: self = .keyVerificationApp case 32: self = .replyApp case 33: self = .ipTunnelApp case 34: self = .paxcounterApp @@ -268,6 +273,7 @@ public enum PortNum: SwiftProtobuf.Enum { case .audioApp: return 9 case .detectionSensorApp: return 10 case .alertApp: return 11 + case .keyVerificationApp: return 12 case .replyApp: return 32 case .ipTunnelApp: return 33 case .paxcounterApp: return 34 @@ -290,11 +296,6 @@ public enum PortNum: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [PortNum] = [ .unknownApp, @@ -309,6 +310,7 @@ extension PortNum: CaseIterable { .audioApp, .detectionSensorApp, .alertApp, + .keyVerificationApp, .replyApp, .ipTunnelApp, .paxcounterApp, @@ -328,14 +330,9 @@ extension PortNum: CaseIterable { .atakForwarder, .max, ] + } -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PortNum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PortNum: SwiftProtobuf._ProtoNameProviding { @@ -352,6 +349,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "AUDIO_APP"), 10: .same(proto: "DETECTION_SENSOR_APP"), 11: .same(proto: "ALERT_APP"), + 12: .same(proto: "KEY_VERIFICATION_APP"), 32: .same(proto: "REPLY_APP"), 33: .same(proto: "IP_TUNNEL_APP"), 34: .same(proto: "PAXCOUNTER_APP"), diff --git a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift index 5f51e948..58c21701 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/powermon.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/powermon.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). ///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) -public struct PowerMon { +public struct PowerMon: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -31,7 +31,7 @@ public struct PowerMon { /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. ///If you are making new meshtastic features feel free to add new entries at the end of this definition. - public enum State: SwiftProtobuf.Enum { + public enum State: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case none // = 0 case cpuDeepSleep // = 1 @@ -104,37 +104,31 @@ public struct PowerMon { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerMon.State: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerMon.State] = [ - .none, - .cpuDeepSleep, - .cpuLightSleep, - .vext1On, - .loraRxon, - .loraTxon, - .loraRxactive, - .btOn, - .ledOn, - .screenOn, - .screenDrawing, - .wifiOn, - .gpsActive, - ] -} - -#endif // swift(>=4.2) - /// /// PowerStress testing support via the C++ PowerStress module -public struct PowerStressMessage { +public struct PowerStressMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -151,7 +145,7 @@ public struct PowerStressMessage { /// What operation would we like the UUT to perform. ///note: senders should probably set want_response in their request packets, so that they can know when the state ///machine has started processing their request - public enum Opcode: SwiftProtobuf.Enum { + public enum Opcode: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -272,48 +266,35 @@ public struct PowerStressMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [PowerStressMessage.Opcode] = [ + .unset, + .printInfo, + .forceQuiet, + .endQuiet, + .screenOn, + .screenOff, + .cpuIdle, + .cpuDeepsleep, + .cpuFullon, + .ledOn, + .ledOff, + .loraOff, + .loraTx, + .loraRx, + .btOff, + .btOn, + .wifiOff, + .wifiOn, + .gpsOff, + .gpsOn, + ] + } public init() {} } -#if swift(>=4.2) - -extension PowerStressMessage.Opcode: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [PowerStressMessage.Opcode] = [ - .unset, - .printInfo, - .forceQuiet, - .endQuiet, - .screenOn, - .screenOff, - .cpuIdle, - .cpuDeepsleep, - .cpuFullon, - .ledOn, - .ledOff, - .loraOff, - .loraTx, - .loraRx, - .btOff, - .btOn, - .wifiOff, - .wifiOn, - .gpsOff, - .gpsOn, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension PowerMon: @unchecked Sendable {} -extension PowerMon.State: @unchecked Sendable {} -extension PowerStressMessage: @unchecked Sendable {} -extension PowerStressMessage.Opcode: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -323,8 +304,8 @@ extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { @@ -379,7 +360,7 @@ extension PowerStressMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.cmd != .unset { try visitor.visitSingularEnumField(value: self.cmd, fieldNumber: 1) } - if self.numSeconds != 0 { + if self.numSeconds.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.numSeconds, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift index ac6eeb26..d23dc07b 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/remote_hardware.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/remote_hardware.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -30,7 +30,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// because no security yet (beyond the channel mechanism). /// It should be off by default and then protected based on some TBD mechanism /// (a special channel once multichannel support is included?) -public struct HardwareMessage { +public struct HardwareMessage: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -52,7 +52,7 @@ public struct HardwareMessage { /// /// TODO: REPLACE - public enum TypeEnum: SwiftProtobuf.Enum { + public enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -110,32 +110,21 @@ public struct HardwareMessage { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [HardwareMessage.TypeEnum] = [ + .unset, + .writeGpios, + .watchGpios, + .gpiosChanged, + .readGpios, + .readGpiosReply, + ] + } public init() {} } -#if swift(>=4.2) - -extension HardwareMessage.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [HardwareMessage.TypeEnum] = [ - .unset, - .writeGpios, - .watchGpios, - .gpiosChanged, - .readGpios, - .readGpiosReply, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension HardwareMessage: @unchecked Sendable {} -extension HardwareMessage.TypeEnum: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift index 6fdf3208..38d0c880 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/rtttl.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/rtttl.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Canned message module configuration. -public struct RTTTLConfig { +public struct RTTTLConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -36,10 +36,6 @@ public struct RTTTLConfig { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension RTTTLConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift index 54efa77b..deb96569 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/storeforward.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/storeforward.proto @@ -22,7 +23,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// TODO: REPLACE -public struct StoreAndForward { +public struct StoreAndForward: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -79,7 +80,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, @unchecked Sendable { /// /// TODO: REPLACE case stats(StoreAndForward.Statistics) @@ -93,38 +94,12 @@ public struct StoreAndForward { /// Text from history message. case text(Data) - #if !swift(>=4.1) - public static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.stats, .stats): return { - guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.history, .history): return { - guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.heartbeat, .heartbeat): return { - guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.text, .text): return { - guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// /// 001 - 063 = From Router /// 064 - 127 = From Client - public enum RequestResponse: SwiftProtobuf.Enum { + public enum RequestResponse: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -242,11 +217,31 @@ public struct StoreAndForward { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [StoreAndForward.RequestResponse] = [ + .unset, + .routerError, + .routerHeartbeat, + .routerPing, + .routerPong, + .routerBusy, + .routerHistory, + .routerStats, + .routerTextDirect, + .routerTextBroadcast, + .clientError, + .clientHistory, + .clientStats, + .clientPing, + .clientPong, + .clientAbort, + ] + } /// /// TODO: REPLACE - public struct Statistics { + public struct Statistics: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -294,7 +289,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct History { + public struct History: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -319,7 +314,7 @@ public struct StoreAndForward { /// /// TODO: REPLACE - public struct Heartbeat { + public struct Heartbeat: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -340,41 +335,6 @@ public struct StoreAndForward { public init() {} } -#if swift(>=4.2) - -extension StoreAndForward.RequestResponse: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [StoreAndForward.RequestResponse] = [ - .unset, - .routerError, - .routerHeartbeat, - .routerPing, - .routerPong, - .routerBusy, - .routerHistory, - .routerStats, - .routerTextDirect, - .routerTextBroadcast, - .clientError, - .clientHistory, - .clientStats, - .clientPing, - .clientPong, - .clientAbort, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension StoreAndForward: @unchecked Sendable {} -extension StoreAndForward.OneOf_Variant: @unchecked Sendable {} -extension StoreAndForward.RequestResponse: @unchecked Sendable {} -extension StoreAndForward.Statistics: @unchecked Sendable {} -extension StoreAndForward.History: @unchecked Sendable {} -extension StoreAndForward.Heartbeat: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index da282fe2..2b89d4bd 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/telemetry.proto @@ -7,7 +8,6 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// /// Supported I2C Sensors for telemetry in Meshtastic -public enum TelemetrySensorType: SwiftProtobuf.Enum { +public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// @@ -176,6 +176,14 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// RAKWireless RAK12035 Soil Moisture Sensor Module case rak12035 // = 37 + + /// + /// MAX17261 lipo battery gauge + case max17261 // = 38 + + /// + /// PCT2075 Temperature Sensor + case pct2075 // = 39 case UNRECOGNIZED(Int) public init() { @@ -222,6 +230,8 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 35: self = .dfrobotRain case 36: self = .dps310 case 37: self = .rak12035 + case 38: self = .max17261 + case 39: self = .pct2075 default: self = .UNRECOGNIZED(rawValue) } } @@ -266,15 +276,12 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .dfrobotRain: return 35 case .dps310: return 36 case .rak12035: return 37 + case .max17261: return 38 + case .pct2075: return 39 case .UNRECOGNIZED(let i): return i } } -} - -#if swift(>=4.2) - -extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [TelemetrySensorType] = [ .sensorUnset, @@ -315,14 +322,15 @@ extension TelemetrySensorType: CaseIterable { .dfrobotRain, .dps310, .rak12035, + .max17261, + .pct2075, ] -} -#endif // swift(>=4.2) +} /// /// Key native device metrics such as battery level -public struct DeviceMetrics { +public struct DeviceMetrics: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -395,7 +403,7 @@ public struct DeviceMetrics { /// /// Weather station or other environmental metrics -public struct EnvironmentMetrics { +public struct EnvironmentMetrics: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -653,7 +661,7 @@ public struct EnvironmentMetrics { /// /// Power Metrics (voltage / current / etc) -public struct PowerMetrics { +public struct PowerMetrics: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -738,7 +746,7 @@ public struct PowerMetrics { /// /// Air quality metrics -public struct AirQualityMetrics { +public struct AirQualityMetrics: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -907,7 +915,7 @@ public struct AirQualityMetrics { /// /// Local device mesh statistics -public struct LocalStats { +public struct LocalStats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -958,6 +966,14 @@ public struct LocalStats { /// This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. public var numTxRelayCanceled: UInt32 = 0 + /// + /// Number of bytes used in the heap + public var heapTotalBytes: UInt32 = 0 + + /// + /// Number of bytes free in the heap + public var heapFreeBytes: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -965,7 +981,7 @@ public struct LocalStats { /// /// Health telemetry metrics -public struct HealthMetrics { +public struct HealthMetrics: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1012,9 +1028,83 @@ public struct HealthMetrics { fileprivate var _temperature: Float? = nil } +/// +/// Linux host metrics +public struct HostMetrics: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// Host system uptime + public var uptimeSeconds: UInt32 = 0 + + /// + /// Host system free memory + public var freememBytes: UInt64 = 0 + + /// + /// Host system disk space free for / + public var diskfree1Bytes: UInt64 = 0 + + /// + /// Secondary system disk space free + public var diskfree2Bytes: UInt64 { + get {return _diskfree2Bytes ?? 0} + set {_diskfree2Bytes = newValue} + } + /// Returns true if `diskfree2Bytes` has been explicitly set. + public var hasDiskfree2Bytes: Bool {return self._diskfree2Bytes != nil} + /// Clears the value of `diskfree2Bytes`. Subsequent reads from it will return its default value. + public mutating func clearDiskfree2Bytes() {self._diskfree2Bytes = nil} + + /// + /// Tertiary disk space free + public var diskfree3Bytes: UInt64 { + get {return _diskfree3Bytes ?? 0} + set {_diskfree3Bytes = newValue} + } + /// Returns true if `diskfree3Bytes` has been explicitly set. + public var hasDiskfree3Bytes: Bool {return self._diskfree3Bytes != nil} + /// Clears the value of `diskfree3Bytes`. Subsequent reads from it will return its default value. + public mutating func clearDiskfree3Bytes() {self._diskfree3Bytes = nil} + + /// + /// Host system one minute load in 1/100ths + public var load1: UInt32 = 0 + + /// + /// Host system five minute load in 1/100ths + public var load5: UInt32 = 0 + + /// + /// Host system fifteen minute load in 1/100ths + public var load15: UInt32 = 0 + + /// + /// Optional User-provided string for arbitrary host system information + /// that doesn't make sense as a dedicated entry. + public var userString: String { + get {return _userString ?? String()} + set {_userString = newValue} + } + /// Returns true if `userString` has been explicitly set. + public var hasUserString: Bool {return self._userString != nil} + /// Clears the value of `userString`. Subsequent reads from it will return its default value. + public mutating func clearUserString() {self._userString = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _diskfree2Bytes: UInt64? = nil + fileprivate var _diskfree3Bytes: UInt64? = nil + fileprivate var _userString: String? = nil +} + /// /// Types of Measurements the telemetry module is equipped to handle -public struct Telemetry { +public struct Telemetry: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1085,9 +1175,19 @@ public struct Telemetry { set {variant = .healthMetrics(newValue)} } + /// + /// Linux host metrics + public var hostMetrics: HostMetrics { + get { + if case .hostMetrics(let v)? = variant {return v} + return HostMetrics() + } + set {variant = .hostMetrics(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum OneOf_Variant: Equatable { + public enum OneOf_Variant: Equatable, Sendable { /// /// Key native device metrics such as battery level case deviceMetrics(DeviceMetrics) @@ -1106,41 +1206,10 @@ public struct Telemetry { /// /// Health telemetry metrics case healthMetrics(HealthMetrics) + /// + /// Linux host metrics + case hostMetrics(HostMetrics) - #if !swift(>=4.1) - public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.deviceMetrics, .deviceMetrics): return { - guard case .deviceMetrics(let l) = lhs, case .deviceMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.environmentMetrics, .environmentMetrics): return { - guard case .environmentMetrics(let l) = lhs, case .environmentMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.airQualityMetrics, .airQualityMetrics): return { - guard case .airQualityMetrics(let l) = lhs, case .airQualityMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.powerMetrics, .powerMetrics): return { - guard case .powerMetrics(let l) = lhs, case .powerMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.localStats, .localStats): return { - guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.healthMetrics, .healthMetrics): return { - guard case .healthMetrics(let l) = lhs, case .healthMetrics(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -1148,7 +1217,7 @@ public struct Telemetry { /// /// NAU7802 Telemetry configuration, for saving to flash -public struct Nau7802Config { +public struct Nau7802Config: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1166,19 +1235,6 @@ public struct Nau7802Config { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension TelemetrySensorType: @unchecked Sendable {} -extension DeviceMetrics: @unchecked Sendable {} -extension EnvironmentMetrics: @unchecked Sendable {} -extension PowerMetrics: @unchecked Sendable {} -extension AirQualityMetrics: @unchecked Sendable {} -extension LocalStats: @unchecked Sendable {} -extension HealthMetrics: @unchecked Sendable {} -extension Telemetry: @unchecked Sendable {} -extension Telemetry.OneOf_Variant: @unchecked Sendable {} -extension Nau7802Config: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1223,6 +1279,8 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 35: .same(proto: "DFROBOT_RAIN"), 36: .same(proto: "DPS310"), 37: .same(proto: "RAK12035"), + 38: .same(proto: "MAX17261"), + 39: .same(proto: "PCT2075"), ] } @@ -1718,6 +1776,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 9: .standard(proto: "num_rx_dupe"), 10: .standard(proto: "num_tx_relay"), 11: .standard(proto: "num_tx_relay_canceled"), + 12: .standard(proto: "heap_total_bytes"), + 13: .standard(proto: "heap_free_bytes"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1737,6 +1797,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 9: try { try decoder.decodeSingularUInt32Field(value: &self.numRxDupe) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelay) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelayCanceled) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.heapTotalBytes) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self.heapFreeBytes) }() default: break } } @@ -1746,10 +1808,10 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.uptimeSeconds != 0 { try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) } - if self.channelUtilization != 0 { + if self.channelUtilization.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.channelUtilization, fieldNumber: 2) } - if self.airUtilTx != 0 { + if self.airUtilTx.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.airUtilTx, fieldNumber: 3) } if self.numPacketsTx != 0 { @@ -1776,6 +1838,12 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.numTxRelayCanceled != 0 { try visitor.visitSingularUInt32Field(value: self.numTxRelayCanceled, fieldNumber: 11) } + if self.heapTotalBytes != 0 { + try visitor.visitSingularUInt32Field(value: self.heapTotalBytes, fieldNumber: 12) + } + if self.heapFreeBytes != 0 { + try visitor.visitSingularUInt32Field(value: self.heapFreeBytes, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -1791,6 +1859,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.numRxDupe != rhs.numRxDupe {return false} if lhs.numTxRelay != rhs.numTxRelay {return false} if lhs.numTxRelayCanceled != rhs.numTxRelayCanceled {return false} + if lhs.heapTotalBytes != rhs.heapTotalBytes {return false} + if lhs.heapFreeBytes != rhs.heapFreeBytes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1844,6 +1914,90 @@ extension HealthMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa } } +extension HostMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HostMetrics" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "uptime_seconds"), + 2: .standard(proto: "freemem_bytes"), + 3: .standard(proto: "diskfree1_bytes"), + 4: .standard(proto: "diskfree2_bytes"), + 5: .standard(proto: "diskfree3_bytes"), + 6: .same(proto: "load1"), + 7: .same(proto: "load5"), + 8: .same(proto: "load15"), + 9: .standard(proto: "user_string"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.uptimeSeconds) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.freememBytes) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.diskfree1Bytes) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self._diskfree2Bytes) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self._diskfree3Bytes) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.load1) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.load5) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.load15) }() + case 9: try { try decoder.decodeSingularStringField(value: &self._userString) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.uptimeSeconds != 0 { + try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) + } + if self.freememBytes != 0 { + try visitor.visitSingularUInt64Field(value: self.freememBytes, fieldNumber: 2) + } + if self.diskfree1Bytes != 0 { + try visitor.visitSingularUInt64Field(value: self.diskfree1Bytes, fieldNumber: 3) + } + try { if let v = self._diskfree2Bytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._diskfree3Bytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5) + } }() + if self.load1 != 0 { + try visitor.visitSingularUInt32Field(value: self.load1, fieldNumber: 6) + } + if self.load5 != 0 { + try visitor.visitSingularUInt32Field(value: self.load5, fieldNumber: 7) + } + if self.load15 != 0 { + try visitor.visitSingularUInt32Field(value: self.load15, fieldNumber: 8) + } + try { if let v = self._userString { + try visitor.visitSingularStringField(value: v, fieldNumber: 9) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: HostMetrics, rhs: HostMetrics) -> Bool { + if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs.freememBytes != rhs.freememBytes {return false} + if lhs.diskfree1Bytes != rhs.diskfree1Bytes {return false} + if lhs._diskfree2Bytes != rhs._diskfree2Bytes {return false} + if lhs._diskfree3Bytes != rhs._diskfree3Bytes {return false} + if lhs.load1 != rhs.load1 {return false} + if lhs.load5 != rhs.load5 {return false} + if lhs.load15 != rhs.load15 {return false} + if lhs._userString != rhs._userString {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Telemetry" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -1854,6 +2008,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 5: .standard(proto: "power_metrics"), 6: .standard(proto: "local_stats"), 7: .standard(proto: "health_metrics"), + 8: .standard(proto: "host_metrics"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1941,6 +2096,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .healthMetrics(v) } }() + case 8: try { + var v: HostMetrics? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .hostMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .hostMetrics(v) + } + }() default: break } } @@ -1979,6 +2147,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .healthMetrics(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 7) }() + case .hostMetrics?: try { + guard case .hostMetrics(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -2016,7 +2188,7 @@ extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa if self.zeroOffset != 0 { try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) } - if self.calibrationFactor != 0 { + if self.calibrationFactor.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift index 1f41fe0b..46907a58 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/xmodem.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: meshtastic/xmodem.proto @@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct XModem { +public struct XModem: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -35,7 +36,7 @@ public struct XModem { public var unknownFields = SwiftProtobuf.UnknownStorage() - public enum Control: SwiftProtobuf.Enum { + public enum Control: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int case nul // = 0 case soh // = 1 @@ -79,34 +80,23 @@ public struct XModem { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [XModem.Control] = [ + .nul, + .soh, + .stx, + .eot, + .ack, + .nak, + .can, + .ctrlz, + ] + } public init() {} } -#if swift(>=4.2) - -extension XModem.Control: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [XModem.Control] = [ - .nul, - .soh, - .stx, - .eot, - .ack, - .nak, - .can, - .ctrlz, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension XModem: @unchecked Sendable {} -extension XModem.Control: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" diff --git a/protobufs b/protobufs index 06864665..816595c8 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 068646653e8375fc145988026ad242a3cf70f7ab +Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 diff --git a/scripts/gen_protos.sh b/scripts/gen_protos.sh index d07bc798..1ce1d1e9 100755 --- a/scripts/gen_protos.sh +++ b/scripts/gen_protos.sh @@ -5,6 +5,10 @@ if [ ! -x "$(which protoc)" ]; then brew install swift-protobuf fi +git submodule update --init --recursive + +git submodule foreach --recursive git pull origin master + protoc --proto_path=./protobufs --swift_opt=Visibility=Public --swift_out=./MeshtasticProtobufs/Sources ./protobufs/meshtastic/*.proto echo "Done generating the swift files from the proto files."